index.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { useMemo } from 'react';
  2. import { useCanvasStore, useSceneSelector } from '@/lib/store';
  3. import {
  4. ElementTypes,
  5. type PPTElement,
  6. type PPTLineElement,
  7. type PPTVideoElement,
  8. type PPTAudioElement,
  9. type PPTShapeElement,
  10. type PPTChartElement,
  11. type Slide,
  12. type PPTAnimation,
  13. } from '@/lib/types/slides';
  14. import type { OperateLineHandlers, OperateResizeHandlers } from '@/lib/types/edit';
  15. import { ImageElementOperate } from './ImageElementOperate';
  16. import { TextElementOperate } from './TextElementOperate';
  17. import { ShapeElementOperate } from './ShapeElementOperate';
  18. import { LineElementOperate } from './LineElementOperate';
  19. import { TableElementOperate } from './TableElementOperate';
  20. import { CommonElementOperate } from './CommonElementOperate';
  21. import type { SlideContent } from '@/lib/types/stage';
  22. interface OperateProps {
  23. readonly elementInfo: PPTElement;
  24. readonly isSelected: boolean;
  25. readonly isActive: boolean;
  26. readonly isActiveGroupElement: boolean;
  27. readonly isMultiSelect: boolean;
  28. readonly rotateElement: (
  29. e: React.MouseEvent,
  30. element: Exclude<
  31. PPTElement,
  32. PPTChartElement | PPTLineElement | PPTVideoElement | PPTAudioElement
  33. >,
  34. ) => void;
  35. readonly scaleElement: (
  36. e: React.MouseEvent,
  37. element: Exclude<PPTElement, PPTLineElement>,
  38. command: OperateResizeHandlers,
  39. ) => void;
  40. readonly dragLineElement: (
  41. e: React.MouseEvent,
  42. element: PPTLineElement,
  43. command: OperateLineHandlers,
  44. ) => void;
  45. readonly moveShapeKeypoint: (
  46. e: React.MouseEvent,
  47. element: PPTShapeElement,
  48. index: number,
  49. ) => void;
  50. readonly openLinkDialog: () => void;
  51. }
  52. export function Operate({
  53. elementInfo,
  54. isSelected,
  55. isActive,
  56. isActiveGroupElement,
  57. isMultiSelect,
  58. rotateElement,
  59. scaleElement,
  60. dragLineElement,
  61. moveShapeKeypoint,
  62. openLinkDialog: _openLinkDialog,
  63. }: OperateProps) {
  64. const canvasScale = useCanvasStore.use.canvasScale();
  65. const toolbarState = useCanvasStore.use.toolbarState();
  66. // Get the formatted animations using a proper selector to avoid infinite loops
  67. const currentSlide = useSceneSelector<SlideContent, Slide>((content) => content.canvas);
  68. const formatedAnimations = useMemo(() => {
  69. if (!currentSlide?.animations) return [];
  70. const els = currentSlide.elements;
  71. const elIds = els.map((el) => el.id);
  72. const animations = currentSlide.animations.filter((animation) =>
  73. elIds.includes(animation.elId),
  74. );
  75. const formatedAnimations: {
  76. animations: PPTAnimation[];
  77. autoNext: boolean;
  78. }[] = [];
  79. for (const animation of animations) {
  80. if (animation.trigger === 'click' || !formatedAnimations.length) {
  81. formatedAnimations.push({ animations: [animation], autoNext: false });
  82. } else if (animation.trigger === 'meantime') {
  83. const last = formatedAnimations[formatedAnimations.length - 1];
  84. last.animations = last.animations.filter((item) => item.elId !== animation.elId);
  85. last.animations.push(animation);
  86. formatedAnimations[formatedAnimations.length - 1] = last;
  87. } else if (animation.trigger === 'auto') {
  88. const last = formatedAnimations[formatedAnimations.length - 1];
  89. last.autoNext = true;
  90. formatedAnimations[formatedAnimations.length - 1] = last;
  91. formatedAnimations.push({ animations: [animation], autoNext: false });
  92. }
  93. }
  94. return formatedAnimations;
  95. }, [currentSlide]);
  96. const CurrentOperateComponent = useMemo(() => {
  97. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- element operate components have varying prop signatures
  98. const elementTypeMap: Record<string, any> = {
  99. [ElementTypes.IMAGE]: ImageElementOperate,
  100. [ElementTypes.TEXT]: TextElementOperate,
  101. [ElementTypes.SHAPE]: ShapeElementOperate,
  102. [ElementTypes.LINE]: LineElementOperate,
  103. [ElementTypes.TABLE]: TableElementOperate,
  104. [ElementTypes.CHART]: CommonElementOperate,
  105. [ElementTypes.LATEX]: CommonElementOperate,
  106. [ElementTypes.VIDEO]: CommonElementOperate,
  107. [ElementTypes.AUDIO]: CommonElementOperate,
  108. };
  109. return elementTypeMap[elementInfo.type] || null;
  110. }, [elementInfo.type]);
  111. const elementIndexListInAnimation = useMemo(() => {
  112. if (!formatedAnimations) return [];
  113. const indexList = [];
  114. for (let i = 0; i < formatedAnimations.length; i++) {
  115. const elIds = formatedAnimations[i].animations.map((item) => item.elId);
  116. if (elIds.includes(elementInfo.id)) indexList.push(i);
  117. }
  118. return indexList;
  119. }, [formatedAnimations, elementInfo.id]);
  120. const rotate = useMemo(() => ('rotate' in elementInfo ? elementInfo.rotate : 0), [elementInfo]);
  121. const height = useMemo(() => ('height' in elementInfo ? elementInfo.height : 0), [elementInfo]);
  122. const handlerVisible = !elementInfo.lock && (isActiveGroupElement || !isMultiSelect);
  123. return (
  124. <div
  125. className={`operate absolute z-43 select-none ${isMultiSelect && !isActive ? 'opacity-20' : ''}`}
  126. style={{
  127. top: elementInfo.top * canvasScale + 'px',
  128. left: elementInfo.left * canvasScale + 'px',
  129. transform: `rotate(${rotate}deg)`,
  130. transformOrigin: `${(elementInfo.width * canvasScale) / 2}px ${(height * canvasScale) / 2}px`,
  131. pointerEvents: 'auto', // Enable mouse events for operate controls
  132. }}
  133. >
  134. {/* eslint-disable @typescript-eslint/no-explicit-any -- dynamic component dispatch requires type widening */}
  135. {isSelected && CurrentOperateComponent && (
  136. <CurrentOperateComponent
  137. elementInfo={elementInfo as any}
  138. handlerVisible={handlerVisible}
  139. rotateElement={rotateElement as any}
  140. scaleElement={scaleElement as any}
  141. dragLineElement={dragLineElement as any}
  142. moveShapeKeypoint={moveShapeKeypoint as any}
  143. />
  144. )}
  145. {/* eslint-enable @typescript-eslint/no-explicit-any */}
  146. {/* Animation index display */}
  147. {toolbarState === 'elAnimation' && elementIndexListInAnimation.length > 0 && (
  148. <div className="animation-index absolute top-0 -left-6 text-xs">
  149. {elementIndexListInAnimation.map((index) => (
  150. <div
  151. key={index}
  152. className="index-item w-[18px] h-[18px] bg-white text-primary border border-primary flex justify-center items-center mt-[5px] first:mt-0"
  153. >
  154. {index + 1}
  155. </div>
  156. ))}
  157. </div>
  158. )}
  159. </div>
  160. );
  161. }