| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- import { create } from 'zustand';
- import { createSelectors } from '@/lib/utils/create-selectors';
- import type { TextAttrs } from '@/lib/prosemirror/utils';
- import { defaultRichTextAttrs } from '@/lib/prosemirror/utils';
- import type { TextFormatPainter, ShapeFormatPainter, CreatingElement } from '@/lib/types/edit';
- import type { PercentageGeometry } from '@/lib/types/action';
- /**
- * Spotlight options
- */
- export interface SpotlightOptions {
- radius?: number; // Spotlight radius (pixels)
- dimness?: number; // Background dimming level (0-1)
- transition?: number; // Transition animation duration (milliseconds)
- }
- /**
- * Highlight overlay options
- */
- export interface HighlightOverlayOptions {
- color?: string; // Highlight color
- opacity?: number; // Highlight opacity (0-1)
- borderWidth?: number; // Border width
- animated?: boolean; // Whether to animate
- }
- /**
- * Laser pointer options
- */
- export interface LaserOptions {
- color?: string; // Laser pointer color, default red
- duration?: number; // Duration (milliseconds)
- }
- /**
- * Canvas Store - Manages all UI state of the Canvas editor
- *
- * Responsibilities:
- * - Element selection state (selected, handling, editing)
- * - Canvas viewport state (zoom, drag, ruler, grid)
- * - Toolbar and panel state
- * - Element being created
- * - Rich text editing state
- * - Format painter state
- *
- * Note: Does not manage slide data (elements, background, etc.), which is managed by Scene Context
- */
- // ==================== Store Interface ====================
- interface CanvasState {
- // ===== Element selection state =====
- activeElementIdList: string[]; // Currently selected element IDs
- handleElementId: string; // Element being operated (drag, resize, etc.)
- activeGroupElementId: string; // Selected child element within a group
- editingElementId: string; // Element being edited (e.g., text editing)
- hiddenElementIdList: string[]; // Hidden element IDs
- // ===== Teaching feature state =====
- spotlightElementId: string; // Element focused by spotlight
- spotlightOptions: SpotlightOptions | null; // Spotlight configuration
- spotlightMode: 'pixel' | 'percentage'; // Spotlight mode: pixel or percentage
- spotlightPercentageGeometry: PercentageGeometry | null; // Percentage mode geometry info
- highlightedElementIds: string[]; // Highlighted element IDs
- highlightOptions: HighlightOverlayOptions | null; // Highlight configuration
- laserElementId: string; // Element focused by laser pointer
- laserOptions: LaserOptions | null; // Laser pointer configuration
- zoomTarget: { elementId: string; scale: number } | null; // Zoom target
- // ===== Canvas viewport state =====
- canvasScale: number; // Canvas actual zoom scale
- canvasPercentage: number; // Canvas percentage (used to calculate canvasScale)
- viewportSize: number; // Viewport width base (default 1000px)
- viewportRatio: number; // Viewport aspect ratio (default 0.5625, i.e. 16:9)
- canvasDragged: boolean; // Whether canvas is being dragged
- // ===== Display aids =====
- showRuler: boolean; // Show ruler
- gridLineSize: number; // Grid line size (0 means hidden)
- // ===== Toolbar and panels =====
- toolbarState: 'design' | 'ai' | 'elAnimation'; // Right toolbar state
- showSelectPanel: boolean; // Selection panel
- showSearchPanel: boolean; // Find and replace panel
- // ===== Element creation =====
- creatingElement: CreatingElement | null; // Element being created (needs draw-to-insert)
- creatingCustomShape: boolean; // Drawing custom shape (arbitrary polygon)
- // ===== Editing state =====
- isScaling: boolean; // Element scaling in progress
- clipingImageElementId: string; // Image being cropped
- richTextAttrs: TextAttrs; // Rich text editing state
- // ===== Format painter =====
- textFormatPainter: TextFormatPainter | null; // Text format painter
- shapeFormatPainter: ShapeFormatPainter | null; // Shape format painter
- // ===== Video playback =====
- playingVideoElementId: string; // Video element currently playing
- // ===== Whiteboard =====
- whiteboardOpen: boolean; // Whether whiteboard is open
- whiteboardClearing: boolean; // Whiteboard clear animation in progress
- // ===== Other =====
- thumbnailsFocus: boolean; // Whether left thumbnail area is focused
- editorAreaFocus: boolean; // Whether editor area is focused
- disableHotkeys: boolean; // Whether hotkeys are disabled
- selectedTableCells: string[]; // Selected table cells
- // ===== Actions =====
- // ----- Element selection -----
- setActiveElementIdList: (ids: string[]) => void;
- setHandleElementId: (id: string) => void;
- setActiveGroupElementId: (id: string) => void;
- setEditingElementId: (id: string) => void;
- setHiddenElementIdList: (ids: string[]) => void;
- clearSelection: () => void; // Clear all selections
- // ----- Canvas viewport -----
- setCanvasScale: (scale: number) => void;
- setCanvasPercentage: (percentage: number) => void;
- setViewportSize: (size: number) => void;
- setViewportRatio: (ratio: number) => void;
- setCanvasDragged: (dragged: boolean) => void;
- // ----- Display aids -----
- setRulerState: (show: boolean) => void;
- setGridLineSize: (size: number) => void;
- // ----- Toolbar and panels -----
- setToolbarState: (state: 'design' | 'ai') => void;
- setSelectPanelState: (show: boolean) => void;
- setSearchPanelState: (show: boolean) => void;
- // ----- Element creation -----
- setCreatingElement: (element: CreatingElement | null) => void;
- setCreatingCustomShapeState: (creating: boolean) => void;
- // ----- Editing state -----
- setScalingState: (isScaling: boolean) => void;
- setClipingImageElementId: (id: string) => void;
- setRichtextAttrs: (attrs: TextAttrs) => void;
- // ----- Format painter -----
- setTextFormatPainter: (painter: TextFormatPainter | null) => void;
- setShapeFormatPainter: (painter: ShapeFormatPainter | null) => void;
- // ----- Video playback -----
- playVideo: (elementId: string) => void;
- pauseVideo: () => void;
- // ----- Whiteboard -----
- setWhiteboardOpen: (open: boolean) => void;
- setWhiteboardClearing: (clearing: boolean) => void;
- // ----- Other -----
- setThumbnailsFocus: (focus: boolean) => void;
- setEditorAreaFocus: (focus: boolean) => void;
- setDisableHotkeysState: (disable: boolean) => void;
- setSelectedTableCells: (cells: string[]) => void;
- // ----- Teaching features -----
- setSpotlight: (elementId: string, options?: SpotlightOptions) => void;
- clearSpotlight: () => void;
- setSpotlightPercentage: (
- elementId: string,
- geometry: PercentageGeometry,
- options?: SpotlightOptions,
- ) => void;
- setHighlight: (elementIds: string[], options?: HighlightOverlayOptions) => void;
- clearHighlight: () => void;
- setLaser: (elementId: string, options?: LaserOptions) => void;
- clearLaser: () => void;
- setZoom: (elementId: string, scale: number) => void;
- clearZoom: () => void;
- clearAllEffects: () => void;
- // ----- Batch operations -----
- resetCanvasState: () => void; // Reset Canvas state (used when switching scenes)
- }
- // ==================== Initial State ====================
- const initialState = {
- // Element selection
- activeElementIdList: [],
- handleElementId: '',
- activeGroupElementId: '',
- editingElementId: '',
- hiddenElementIdList: [],
- // Canvas viewport
- canvasScale: 1,
- canvasPercentage: 90,
- viewportSize: 1000,
- viewportRatio: 0.5625, // 16:9
- canvasDragged: false,
- // Display aids
- showRuler: false,
- gridLineSize: 0,
- // Toolbar and panels
- toolbarState: 'ai' as const,
- showSelectPanel: false,
- showSearchPanel: false,
- // Element creation
- creatingElement: null,
- creatingCustomShape: false,
- // Editing state
- isScaling: false,
- clipingImageElementId: '',
- richTextAttrs: defaultRichTextAttrs,
- // Format painter
- textFormatPainter: null,
- shapeFormatPainter: null,
- // Video playback
- playingVideoElementId: '',
- // Whiteboard
- whiteboardOpen: false,
- whiteboardClearing: false,
- // Other: false,
- editorAreaFocus: false,
- thumbnailsFocus: false,
- disableHotkeys: false,
- selectedTableCells: [],
- // Teaching features
- spotlightElementId: '',
- spotlightOptions: null,
- spotlightMode: 'pixel' as const,
- spotlightPercentageGeometry: null,
- highlightedElementIds: [],
- highlightOptions: null,
- laserElementId: '',
- laserOptions: null,
- zoomTarget: null,
- };
- // ==================== Store Implementation ====================
- const useCanvasStoreBase = create<CanvasState>((set, get) => ({
- ...initialState,
- // ===== Element Selection Actions =====
- setActiveElementIdList: (ids) => {
- set({ activeElementIdList: ids });
- // Auto-set handleElementId: set to that element for single select, empty for multi-select or none
- if (ids.length === 1) {
- set({ handleElementId: ids[0] });
- } else if (ids.length === 0) {
- set({ handleElementId: '' });
- }
- // Auto-switch to design panel when elements are selected
- if (ids.length > 0) {
- set({ toolbarState: 'design' });
- }
- },
- setHandleElementId: (id) => set({ handleElementId: id }),
- setActiveGroupElementId: (id) => set({ activeGroupElementId: id }),
- setEditingElementId: (id) => set({ editingElementId: id }),
- setHiddenElementIdList: (ids) => set({ hiddenElementIdList: ids }),
- clearSelection: () => {
- set({
- activeElementIdList: [],
- handleElementId: '',
- activeGroupElementId: '',
- editingElementId: '',
- });
- },
- // ===== Canvas Viewport Actions =====
- setCanvasScale: (scale) => set({ canvasScale: scale }),
- setCanvasPercentage: (percentage) => set({ canvasPercentage: percentage }),
- setViewportSize: (size) => set({ viewportSize: size }),
- setViewportRatio: (ratio) => set({ viewportRatio: ratio }),
- setCanvasDragged: (dragged) => set({ canvasDragged: dragged }),
- // ===== Display Aids Actions =====
- setRulerState: (show) => set({ showRuler: show }),
- setGridLineSize: (size) => set({ gridLineSize: size }),
- // ===== Toolbar and Panel Actions =====
- setToolbarState: (toolbarState) => set({ toolbarState }),
- setSelectPanelState: (show) => set({ showSelectPanel: show }),
- setSearchPanelState: (show) => set({ showSearchPanel: show }),
- // ===== Element Creation Actions =====
- setCreatingElement: (element) => set({ creatingElement: element }),
- setCreatingCustomShapeState: (creating) => set({ creatingCustomShape: creating }),
- // ===== Editing State Actions =====
- setScalingState: (isScaling) => set({ isScaling }),
- setClipingImageElementId: (id) => set({ clipingImageElementId: id }),
- setRichtextAttrs: (attrs) => set({ richTextAttrs: attrs }),
- // ===== Format Painter Actions =====
- setTextFormatPainter: (painter) => set({ textFormatPainter: painter }),
- setShapeFormatPainter: (painter) => set({ shapeFormatPainter: painter }),
- // ===== Video Playback Actions =====
- playVideo: (elementId) => set({ playingVideoElementId: elementId }),
- pauseVideo: () => set({ playingVideoElementId: '' }),
- // ===== Whiteboard Actions =====
- setWhiteboardOpen: (open) => set({ whiteboardOpen: open }),
- setWhiteboardClearing: (clearing) => set({ whiteboardClearing: clearing }),
- // ===== Other Actions =====
- setThumbnailsFocus: (focus) => set({ thumbnailsFocus: focus }),
- setEditorAreaFocus: (focus) => set({ editorAreaFocus: focus }),
- setDisableHotkeysState: (disable) => set({ disableHotkeys: disable }),
- setSelectedTableCells: (cells) => set({ selectedTableCells: cells }),
- // ===== Teaching Feature Actions =====
- setSpotlight: (elementId, options = {}) => {
- set({
- spotlightElementId: elementId,
- spotlightMode: 'pixel',
- spotlightOptions: {
- radius: 200,
- dimness: 0.7,
- transition: 300,
- ...options,
- },
- spotlightPercentageGeometry: null,
- });
- },
- setSpotlightPercentage: (elementId, geometry, options = {}) => {
- set({
- spotlightElementId: elementId,
- spotlightMode: 'percentage',
- spotlightPercentageGeometry: geometry,
- spotlightOptions: {
- dimness: 0.7,
- transition: 300,
- ...options,
- },
- });
- },
- clearSpotlight: () => {
- set({
- spotlightElementId: '',
- spotlightOptions: null,
- spotlightMode: 'pixel',
- spotlightPercentageGeometry: null,
- });
- },
- setHighlight: (elementIds, options = {}) => {
- set({
- highlightedElementIds: elementIds,
- highlightOptions: {
- color: '#ff6b6b',
- opacity: 0.3,
- borderWidth: 3,
- animated: true,
- ...options,
- },
- });
- },
- clearHighlight: () => {
- set({
- highlightedElementIds: [],
- highlightOptions: null,
- });
- },
- setLaser: (elementId, options = {}) => {
- set({
- laserElementId: elementId,
- laserOptions: {
- color: '#ff0000',
- duration: 3000,
- ...options,
- },
- });
- },
- clearLaser: () => {
- set({
- laserElementId: '',
- laserOptions: null,
- });
- },
- setZoom: (elementId, scale) => {
- set({
- zoomTarget: { elementId, scale },
- });
- },
- clearZoom: () => {
- set({
- zoomTarget: null,
- });
- },
- clearAllEffects: () => {
- set({
- spotlightElementId: '',
- spotlightOptions: null,
- spotlightMode: 'pixel',
- spotlightPercentageGeometry: null,
- highlightedElementIds: [],
- highlightOptions: null,
- laserElementId: '',
- laserOptions: null,
- zoomTarget: null,
- // Note: playingVideoElementId intentionally NOT cleared here.
- // Video playback has its own lifecycle (playVideo/pauseVideo/onEnded)
- // and must not be interrupted by visual effect auto-clear timers.
- });
- },
- // ===== Batch Operations =====
- resetCanvasState: () => {
- set({
- ...initialState,
- // Preserve viewport settings
- viewportSize: get().viewportSize,
- viewportRatio: get().viewportRatio,
- });
- },
- }));
- // Enhance store with selectors, supporting store.use.xxx() syntax
- export const useCanvasStore = createSelectors(useCanvasStoreBase);
|