diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | bfc2cec7d43eb8eaa46dd3f91084932381257059 (patch) | |
| tree | 0857e3aac2cff922826d4871ff54536b26fad6fc /excalidraw-app/components/DebugCanvas.tsx | |
| parent | 225db4a7805befe009fe055fc2ef5daedd6c04f9 (diff) | |
refactor: excalidraw-app/
Diffstat (limited to 'excalidraw-app/components/DebugCanvas.tsx')
| -rw-r--r-- | excalidraw-app/components/DebugCanvas.tsx | 344 |
1 files changed, 344 insertions, 0 deletions
diff --git a/excalidraw-app/components/DebugCanvas.tsx b/excalidraw-app/components/DebugCanvas.tsx new file mode 100644 index 0000000..31898ba --- /dev/null +++ b/excalidraw-app/components/DebugCanvas.tsx @@ -0,0 +1,344 @@ +import { useCallback, useImperativeHandle, useRef } from "react"; +import { type AppState } from "@excalidraw/excalidraw/types"; +import { throttleRAF } from "@excalidraw/excalidraw/utils"; +import { + bootstrapCanvas, + getNormalizedCanvasDimensions, +} from "@excalidraw/excalidraw/renderer/helpers"; +import type { DebugElement } from "@excalidraw/excalidraw/visualdebug"; +import { + ArrowheadArrowIcon, + CloseIcon, + TrashIcon, +} from "@excalidraw/excalidraw/components/icons"; +import { STORAGE_KEYS } from "../app_constants"; +import type { Curve } from "../../packages/math"; +import { + isLineSegment, + type GlobalPoint, + type LineSegment, +} from "../../packages/math"; +import { isCurve } from "../../packages/math/curve"; + +const renderLine = ( + context: CanvasRenderingContext2D, + zoom: number, + segment: LineSegment<GlobalPoint>, + color: string, +) => { + context.save(); + context.strokeStyle = color; + context.beginPath(); + context.moveTo(segment[0][0] * zoom, segment[0][1] * zoom); + context.lineTo(segment[1][0] * zoom, segment[1][1] * zoom); + context.stroke(); + context.restore(); +}; + +const renderCubicBezier = ( + context: CanvasRenderingContext2D, + zoom: number, + [start, control1, control2, end]: Curve<GlobalPoint>, + color: string, +) => { + context.save(); + context.strokeStyle = color; + context.beginPath(); + context.moveTo(start[0] * zoom, start[1] * zoom); + context.bezierCurveTo( + control1[0] * zoom, + control1[1] * zoom, + control2[0] * zoom, + control2[1] * zoom, + end[0] * zoom, + end[1] * zoom, + ); + context.stroke(); + context.restore(); +}; + +const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => { + context.strokeStyle = "#888"; + context.save(); + context.beginPath(); + context.moveTo(-10 * zoom, -10 * zoom); + context.lineTo(10 * zoom, 10 * zoom); + context.moveTo(10 * zoom, -10 * zoom); + context.lineTo(-10 * zoom, 10 * zoom); + context.stroke(); + context.save(); +}; + +const render = ( + frame: DebugElement[], + context: CanvasRenderingContext2D, + appState: AppState, +) => { + frame.forEach((el: DebugElement) => { + switch (true) { + case isLineSegment(el.data): + renderLine( + context, + appState.zoom.value, + el.data as LineSegment<GlobalPoint>, + el.color, + ); + break; + case isCurve(el.data): + renderCubicBezier( + context, + appState.zoom.value, + el.data as Curve<GlobalPoint>, + el.color, + ); + break; + default: + throw new Error(`Unknown element type ${JSON.stringify(el)}`); + } + }); +}; + +const _debugRenderer = ( + canvas: HTMLCanvasElement, + appState: AppState, + scale: number, + refresh: () => void, +) => { + const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( + canvas, + scale, + ); + + if (appState.height !== canvas.height || appState.width !== canvas.width) { + refresh(); + } + + const context = bootstrapCanvas({ + canvas, + scale, + normalizedWidth, + normalizedHeight, + viewBackgroundColor: "transparent", + }); + + // Apply zoom + context.save(); + context.translate( + appState.scrollX * appState.zoom.value, + appState.scrollY * appState.zoom.value, + ); + + renderOrigin(context, appState.zoom.value); + + if ( + window.visualDebug?.currentFrame && + window.visualDebug?.data && + window.visualDebug.data.length > 0 + ) { + // Render only one frame + const [idx] = debugFrameData(); + + render(window.visualDebug.data[idx], context, appState); + } else { + // Render all debug frames + window.visualDebug?.data.forEach((frame) => { + render(frame, context, appState); + }); + } + + if (window.visualDebug) { + window.visualDebug!.data = + window.visualDebug?.data.map((frame) => + frame.filter((el) => el.permanent), + ) ?? []; + } +}; + +const debugFrameData = (): [number, number] => { + const currentFrame = window.visualDebug?.currentFrame ?? 0; + const frameCount = window.visualDebug?.data.length ?? 0; + + if (frameCount > 0) { + return [currentFrame % frameCount, window.visualDebug?.currentFrame ?? 0]; + } + + return [0, 0]; +}; + +export const saveDebugState = (debug: { enabled: boolean }) => { + try { + localStorage.setItem( + STORAGE_KEYS.LOCAL_STORAGE_DEBUG, + JSON.stringify(debug), + ); + } catch (error: any) { + console.error(error); + } +}; + +export const debugRenderer = throttleRAF( + ( + canvas: HTMLCanvasElement, + appState: AppState, + scale: number, + refresh: () => void, + ) => { + _debugRenderer(canvas, appState, scale, refresh); + }, + { trailing: true }, +); + +export const loadSavedDebugState = () => { + let debug; + try { + const savedDebugState = localStorage.getItem( + STORAGE_KEYS.LOCAL_STORAGE_DEBUG, + ); + if (savedDebugState) { + debug = JSON.parse(savedDebugState) as { enabled: boolean }; + } + } catch (error: any) { + console.error(error); + } + + return debug ?? { enabled: false }; +}; + +export const isVisualDebuggerEnabled = () => + Array.isArray(window.visualDebug?.data); + +export const DebugFooter = ({ onChange }: { onChange: () => void }) => { + const moveForward = useCallback(() => { + if ( + !window.visualDebug?.currentFrame || + isNaN(window.visualDebug?.currentFrame ?? -1) + ) { + window.visualDebug!.currentFrame = 0; + } + window.visualDebug!.currentFrame += 1; + onChange(); + }, [onChange]); + const moveBackward = useCallback(() => { + if ( + !window.visualDebug?.currentFrame || + isNaN(window.visualDebug?.currentFrame ?? -1) || + window.visualDebug?.currentFrame < 1 + ) { + window.visualDebug!.currentFrame = 1; + } + window.visualDebug!.currentFrame -= 1; + onChange(); + }, [onChange]); + const reset = useCallback(() => { + window.visualDebug!.currentFrame = undefined; + onChange(); + }, [onChange]); + const trashFrames = useCallback(() => { + if (window.visualDebug) { + window.visualDebug.currentFrame = undefined; + window.visualDebug.data = []; + } + onChange(); + }, [onChange]); + + return ( + <> + <button + className="ToolIcon_type_button" + data-testid="debug-forward" + aria-label="Move forward" + type="button" + onClick={trashFrames} + > + <div + className="ToolIcon__icon" + aria-hidden="true" + aria-disabled="false" + > + {TrashIcon} + </div> + </button> + <button + className="ToolIcon_type_button" + data-testid="debug-forward" + aria-label="Move forward" + type="button" + onClick={moveBackward} + > + <div + className="ToolIcon__icon" + aria-hidden="true" + aria-disabled="false" + > + <ArrowheadArrowIcon flip /> + </div> + </button> + <button + className="ToolIcon_type_button" + data-testid="debug-forward" + aria-label="Move forward" + type="button" + onClick={reset} + > + <div + className="ToolIcon__icon" + aria-hidden="true" + aria-disabled="false" + > + {CloseIcon} + </div> + </button> + <button + className="ToolIcon_type_button" + data-testid="debug-backward" + aria-label="Move backward" + type="button" + onClick={moveForward} + > + <div + className="ToolIcon__icon" + aria-hidden="true" + aria-disabled="false" + > + <ArrowheadArrowIcon /> + </div> + </button> + </> + ); +}; + +interface DebugCanvasProps { + appState: AppState; + scale: number; + ref?: React.Ref<HTMLCanvasElement>; +} + +const DebugCanvas = ({ appState, scale, ref }: DebugCanvasProps) => { + const { width, height } = appState; + + const canvasRef = useRef<HTMLCanvasElement>(null); + useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>( + ref, + () => canvasRef.current, + [canvasRef], + ); + + return ( + <canvas + style={{ + width, + height, + position: "absolute", + zIndex: 2, + pointerEvents: "none", + }} + width={width * scale} + height={height * scale} + ref={canvasRef} + > + Debug Canvas + </canvas> + ); +}; + +export default DebugCanvas; |
