DebugCanvas.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
  2. import { type AppState } from "../../packages/excalidraw/types";
  3. import { throttleRAF } from "../../packages/excalidraw/utils";
  4. import {
  5. bootstrapCanvas,
  6. getNormalizedCanvasDimensions,
  7. } from "../../packages/excalidraw/renderer/helpers";
  8. import type { DebugElement } from "../../packages/excalidraw/visualdebug";
  9. import {
  10. ArrowheadArrowIcon,
  11. CloseIcon,
  12. TrashIcon,
  13. } from "../../packages/excalidraw/components/icons";
  14. import { STORAGE_KEYS } from "../app_constants";
  15. import {
  16. isLineSegment,
  17. type GlobalPoint,
  18. type LineSegment,
  19. } from "../../packages/math";
  20. const renderLine = (
  21. context: CanvasRenderingContext2D,
  22. zoom: number,
  23. segment: LineSegment<GlobalPoint>,
  24. color: string,
  25. ) => {
  26. context.save();
  27. context.strokeStyle = color;
  28. context.beginPath();
  29. context.moveTo(segment[0][0] * zoom, segment[0][1] * zoom);
  30. context.lineTo(segment[1][0] * zoom, segment[1][1] * zoom);
  31. context.stroke();
  32. context.restore();
  33. };
  34. const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
  35. context.strokeStyle = "#888";
  36. context.save();
  37. context.beginPath();
  38. context.moveTo(-10 * zoom, -10 * zoom);
  39. context.lineTo(10 * zoom, 10 * zoom);
  40. context.moveTo(10 * zoom, -10 * zoom);
  41. context.lineTo(-10 * zoom, 10 * zoom);
  42. context.stroke();
  43. context.save();
  44. };
  45. const render = (
  46. frame: DebugElement[],
  47. context: CanvasRenderingContext2D,
  48. appState: AppState,
  49. ) => {
  50. frame.forEach((el: DebugElement) => {
  51. switch (true) {
  52. case isLineSegment(el.data):
  53. renderLine(
  54. context,
  55. appState.zoom.value,
  56. el.data as LineSegment<GlobalPoint>,
  57. el.color,
  58. );
  59. break;
  60. }
  61. });
  62. };
  63. const _debugRenderer = (
  64. canvas: HTMLCanvasElement,
  65. appState: AppState,
  66. scale: number,
  67. refresh: () => void,
  68. ) => {
  69. const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
  70. canvas,
  71. scale,
  72. );
  73. if (appState.height !== canvas.height || appState.width !== canvas.width) {
  74. refresh();
  75. }
  76. const context = bootstrapCanvas({
  77. canvas,
  78. scale,
  79. normalizedWidth,
  80. normalizedHeight,
  81. canvasBackgroundColor: "transparent",
  82. });
  83. // Apply zoom
  84. context.save();
  85. context.translate(
  86. appState.scrollX * appState.zoom.value,
  87. appState.scrollY * appState.zoom.value,
  88. );
  89. renderOrigin(context, appState.zoom.value);
  90. if (
  91. window.visualDebug?.currentFrame &&
  92. window.visualDebug?.data &&
  93. window.visualDebug.data.length > 0
  94. ) {
  95. // Render only one frame
  96. const [idx] = debugFrameData();
  97. render(window.visualDebug.data[idx], context, appState);
  98. } else {
  99. // Render all debug frames
  100. window.visualDebug?.data.forEach((frame) => {
  101. render(frame, context, appState);
  102. });
  103. }
  104. if (window.visualDebug) {
  105. window.visualDebug!.data =
  106. window.visualDebug?.data.map((frame) =>
  107. frame.filter((el) => el.permanent),
  108. ) ?? [];
  109. }
  110. };
  111. const debugFrameData = (): [number, number] => {
  112. const currentFrame = window.visualDebug?.currentFrame ?? 0;
  113. const frameCount = window.visualDebug?.data.length ?? 0;
  114. if (frameCount > 0) {
  115. return [currentFrame % frameCount, window.visualDebug?.currentFrame ?? 0];
  116. }
  117. return [0, 0];
  118. };
  119. export const saveDebugState = (debug: { enabled: boolean }) => {
  120. try {
  121. localStorage.setItem(
  122. STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
  123. JSON.stringify(debug),
  124. );
  125. } catch (error: any) {
  126. console.error(error);
  127. }
  128. };
  129. export const debugRenderer = throttleRAF(
  130. (
  131. canvas: HTMLCanvasElement,
  132. appState: AppState,
  133. scale: number,
  134. refresh: () => void,
  135. ) => {
  136. _debugRenderer(canvas, appState, scale, refresh);
  137. },
  138. { trailing: true },
  139. );
  140. export const loadSavedDebugState = () => {
  141. let debug;
  142. try {
  143. const savedDebugState = localStorage.getItem(
  144. STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
  145. );
  146. if (savedDebugState) {
  147. debug = JSON.parse(savedDebugState) as { enabled: boolean };
  148. }
  149. } catch (error: any) {
  150. console.error(error);
  151. }
  152. return debug ?? { enabled: false };
  153. };
  154. export const isVisualDebuggerEnabled = () =>
  155. Array.isArray(window.visualDebug?.data);
  156. export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
  157. const moveForward = useCallback(() => {
  158. if (
  159. !window.visualDebug?.currentFrame ||
  160. isNaN(window.visualDebug?.currentFrame ?? -1)
  161. ) {
  162. window.visualDebug!.currentFrame = 0;
  163. }
  164. window.visualDebug!.currentFrame += 1;
  165. onChange();
  166. }, [onChange]);
  167. const moveBackward = useCallback(() => {
  168. if (
  169. !window.visualDebug?.currentFrame ||
  170. isNaN(window.visualDebug?.currentFrame ?? -1) ||
  171. window.visualDebug?.currentFrame < 1
  172. ) {
  173. window.visualDebug!.currentFrame = 1;
  174. }
  175. window.visualDebug!.currentFrame -= 1;
  176. onChange();
  177. }, [onChange]);
  178. const reset = useCallback(() => {
  179. window.visualDebug!.currentFrame = undefined;
  180. onChange();
  181. }, [onChange]);
  182. const trashFrames = useCallback(() => {
  183. if (window.visualDebug) {
  184. window.visualDebug.currentFrame = undefined;
  185. window.visualDebug.data = [];
  186. }
  187. onChange();
  188. }, [onChange]);
  189. return (
  190. <>
  191. <button
  192. className="ToolIcon_type_button"
  193. data-testid="debug-forward"
  194. aria-label="Move forward"
  195. type="button"
  196. onClick={trashFrames}
  197. >
  198. <div
  199. className="ToolIcon__icon"
  200. aria-hidden="true"
  201. aria-disabled="false"
  202. >
  203. {TrashIcon}
  204. </div>
  205. </button>
  206. <button
  207. className="ToolIcon_type_button"
  208. data-testid="debug-forward"
  209. aria-label="Move forward"
  210. type="button"
  211. onClick={moveBackward}
  212. >
  213. <div
  214. className="ToolIcon__icon"
  215. aria-hidden="true"
  216. aria-disabled="false"
  217. >
  218. <ArrowheadArrowIcon flip />
  219. </div>
  220. </button>
  221. <button
  222. className="ToolIcon_type_button"
  223. data-testid="debug-forward"
  224. aria-label="Move forward"
  225. type="button"
  226. onClick={reset}
  227. >
  228. <div
  229. className="ToolIcon__icon"
  230. aria-hidden="true"
  231. aria-disabled="false"
  232. >
  233. {CloseIcon}
  234. </div>
  235. </button>
  236. <button
  237. className="ToolIcon_type_button"
  238. data-testid="debug-backward"
  239. aria-label="Move backward"
  240. type="button"
  241. onClick={moveForward}
  242. >
  243. <div
  244. className="ToolIcon__icon"
  245. aria-hidden="true"
  246. aria-disabled="false"
  247. >
  248. <ArrowheadArrowIcon />
  249. </div>
  250. </button>
  251. </>
  252. );
  253. };
  254. interface DebugCanvasProps {
  255. appState: AppState;
  256. scale: number;
  257. }
  258. const DebugCanvas = forwardRef<HTMLCanvasElement, DebugCanvasProps>(
  259. ({ appState, scale }, ref) => {
  260. const { width, height } = appState;
  261. const canvasRef = useRef<HTMLCanvasElement>(null);
  262. useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
  263. ref,
  264. () => canvasRef.current,
  265. [canvasRef],
  266. );
  267. return (
  268. <canvas
  269. style={{
  270. width,
  271. height,
  272. position: "absolute",
  273. zIndex: 2,
  274. pointerEvents: "none",
  275. }}
  276. width={width * scale}
  277. height={height * scale}
  278. ref={canvasRef}
  279. >
  280. Debug Canvas
  281. </canvas>
  282. );
  283. },
  284. );
  285. export default DebugCanvas;