parent
26d2296578
commit
ea7c702cfc
@ -0,0 +1,293 @@
|
|||||||
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||||
|
import { type AppState } from "../../packages/excalidraw/types";
|
||||||
|
import { throttleRAF } from "../../packages/excalidraw/utils";
|
||||||
|
import type { LineSegment } from "../../packages/utils";
|
||||||
|
import {
|
||||||
|
bootstrapCanvas,
|
||||||
|
getNormalizedCanvasDimensions,
|
||||||
|
} from "../../packages/excalidraw/renderer/helpers";
|
||||||
|
import type { DebugElement } from "../../packages/excalidraw/visualdebug";
|
||||||
|
import {
|
||||||
|
ArrowheadArrowIcon,
|
||||||
|
CloseIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "../../packages/excalidraw/components/icons";
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
import { isLineSegment } from "../../packages/excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
const renderLine = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
zoom: number,
|
||||||
|
segment: LineSegment,
|
||||||
|
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 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) => {
|
||||||
|
switch (true) {
|
||||||
|
case isLineSegment(el.data):
|
||||||
|
renderLine(context, appState.zoom.value, el.data, el.color);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _debugRenderer = (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
appState: AppState,
|
||||||
|
scale: number,
|
||||||
|
) => {
|
||||||
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
|
canvas,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
_debugRenderer(canvas, appState, scale);
|
||||||
|
},
|
||||||
|
{ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DebugCanvas = forwardRef<HTMLCanvasElement, DebugCanvasProps>(
|
||||||
|
({ appState, scale }, ref) => {
|
||||||
|
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;
|
@ -0,0 +1,157 @@
|
|||||||
|
import type { LineSegment } from "../utils";
|
||||||
|
import type { BoundingBox, Bounds } from "./element/bounds";
|
||||||
|
import { isBounds, isLineSegment } from "./element/typeChecks";
|
||||||
|
import type { Point } from "./types";
|
||||||
|
|
||||||
|
// The global data holder to collect the debug operations
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
visualDebug?: {
|
||||||
|
data: DebugElement[][];
|
||||||
|
currentFrame?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DebugElement = {
|
||||||
|
color: string;
|
||||||
|
data: LineSegment;
|
||||||
|
permanent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawLine = (
|
||||||
|
segment: LineSegment | LineSegment[],
|
||||||
|
opts?: {
|
||||||
|
color?: string;
|
||||||
|
permanent?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
(isLineSegment(segment) ? [segment] : segment).forEach((data) =>
|
||||||
|
addToCurrentFrame({
|
||||||
|
color: opts?.color ?? "red",
|
||||||
|
data,
|
||||||
|
permanent: !!opts?.permanent,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawPoint = (
|
||||||
|
point: Point,
|
||||||
|
opts?: {
|
||||||
|
color?: string;
|
||||||
|
permanent?: boolean;
|
||||||
|
fuzzy?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const xOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
||||||
|
const yOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
||||||
|
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[point[0] + xOffset - 10, point[1] + yOffset - 10],
|
||||||
|
[point[0] + xOffset + 10, point[1] + yOffset + 10],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "cyan",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[point[0] + xOffset - 10, point[1] + yOffset + 10],
|
||||||
|
[point[0] + xOffset + 10, point[1] + yOffset - 10],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "cyan",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawBoundingBox = (
|
||||||
|
box: BoundingBox | BoundingBox[],
|
||||||
|
opts?: {
|
||||||
|
color?: string;
|
||||||
|
permanent?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
(Array.isArray(box) ? box : [box]).forEach((bbox) =>
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[bbox.minX, bbox.minY],
|
||||||
|
[bbox.maxX, bbox.minY],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox.maxX, bbox.minY],
|
||||||
|
[bbox.maxX, bbox.maxY],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox.maxX, bbox.maxY],
|
||||||
|
[bbox.minX, bbox.maxY],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox.minX, bbox.maxY],
|
||||||
|
[bbox.minX, bbox.minY],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "cyan",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawBounds = (
|
||||||
|
box: Bounds | Bounds[],
|
||||||
|
opts?: {
|
||||||
|
color: string;
|
||||||
|
permanent: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[bbox[0], bbox[1]],
|
||||||
|
[bbox[2], bbox[1]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox[2], bbox[1]],
|
||||||
|
[bbox[2], bbox[3]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox[2], bbox[3]],
|
||||||
|
[bbox[0], bbox[3]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox[0], bbox[3]],
|
||||||
|
[bbox[0], bbox[1]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "green",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugCloseFrame = () => {
|
||||||
|
window.visualDebug?.data.push([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugClear = () => {
|
||||||
|
if (window.visualDebug?.data) {
|
||||||
|
window.visualDebug.data = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToCurrentFrame = (element: DebugElement) => {
|
||||||
|
if (window.visualDebug?.data && window.visualDebug.data.length === 0) {
|
||||||
|
window.visualDebug.data[0] = [];
|
||||||
|
}
|
||||||
|
window.visualDebug?.data &&
|
||||||
|
window.visualDebug.data[window.visualDebug.data.length - 1].push(element);
|
||||||
|
};
|
Loading…
Reference in New Issue