diff --git a/excalidraw-app/components/DebugCanvas.tsx b/excalidraw-app/components/DebugCanvas.tsx index 9df430376b..2c157e34e7 100644 --- a/excalidraw-app/components/DebugCanvas.tsx +++ b/excalidraw-app/components/DebugCanvas.tsx @@ -27,7 +27,10 @@ import { isCurve } from "@excalidraw/math/curve"; import React from "react"; import type { Curve } from "@excalidraw/math"; -import type { DebugElement } from "@excalidraw/common"; +import type { + DebugElement, + DebugPolygon, +} from "@excalidraw/element/visualdebug"; import type { ElementsMap, ExcalidrawArrowElement, @@ -75,6 +78,44 @@ const renderCubicBezier = ( context.restore(); }; +const renderPolygon = ( + context: CanvasRenderingContext2D, + zoom: number, + polygon: DebugPolygon, + color: string, +) => { + const { points, fill, close } = polygon; + + if (points.length < 2) { + return; + } + + context.save(); + context.beginPath(); + context.moveTo(points[0][0] * zoom, points[0][1] * zoom); + for (let i = 1; i < points.length; i += 1) { + context.lineTo(points[i][0] * zoom, points[i][1] * zoom); + } + if (close !== false) { + context.closePath(); + } + + if (fill) { + context.save(); + context.globalAlpha = 0.15; + context.fillStyle = color; + context.fill(); + context.restore(); + } + + context.strokeStyle = color; + context.stroke(); + context.restore(); +}; + +const isDebugPolygon = (data: DebugElement["data"]): data is DebugPolygon => + (data as DebugPolygon).type === "polygon"; + const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => { context.strokeStyle = "#888"; context.save(); @@ -280,6 +321,9 @@ const render = ( el.color, ); break; + case isDebugPolygon(el.data): + renderPolygon(context, appState.zoom.value, el.data, el.color); + break; default: throw new Error(`Unknown element type ${JSON.stringify(el)}`); } diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 2539f37ec8..7d6bf5b0dc 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -11,5 +11,4 @@ export * from "./random"; export * from "./url"; export * from "./utils"; export * from "./emitter"; -export * from "./visualdebug"; export * from "./editorInterface"; diff --git a/packages/element/package.json b/packages/element/package.json index 88e3ffaaa8..7dff00a750 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -17,6 +17,12 @@ "development": "./dist/dev/index.js", "production": "./dist/prod/index.js", "default": "./dist/prod/index.js" + }, + "./visualdebug": { + "types": "./dist/types/element/src/visualdebug.d.ts", + "development": "./dist/dev/visualdebug.js", + "production": "./dist/prod/visualdebug.js", + "default": "./dist/prod/visualdebug.js" } }, "files": [ diff --git a/packages/common/src/visualdebug.ts b/packages/element/src/visualdebug.ts similarity index 60% rename from packages/common/src/visualdebug.ts rename to packages/element/src/visualdebug.ts index 1016be52b5..695c6f9f43 100644 --- a/packages/common/src/visualdebug.ts +++ b/packages/element/src/visualdebug.ts @@ -1,16 +1,24 @@ import { isLineSegment, lineSegment, + pointDistanceSq, pointFrom, type GlobalPoint, type LocalPoint, } from "@excalidraw/math"; +import { type Bounds, isBounds } from "@excalidraw/common"; +import { + getElementBounds, + intersectElementWithLineSegment, + isFreeDrawElement, + isLinearElement, + isPathALoop, +} from "@excalidraw/element"; +import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import type { Curve } from "@excalidraw/math"; import type { LineSegment } from "@excalidraw/utils"; -import { type Bounds, isBounds } from "./bounds"; - // The global data holder to collect the debug operations declare global { interface Window { @@ -23,10 +31,69 @@ declare global { export type DebugElement = { color: string; - data: LineSegment | Curve; + data: LineSegment | Curve | DebugPolygon; permanent: boolean; }; +export type DebugPolygon = { + type: "polygon"; + points: GlobalPoint[]; + fill?: boolean; + close?: boolean; +}; + +export const debugDrawHitVolume = ( + element: ExcalidrawElement, + elementsMap: ElementsMap, + options?: { + rays?: number; + color?: string; + fill?: boolean; + }, +) => { + if ( + (isLinearElement(element) || isFreeDrawElement(element)) && + !isPathALoop(element.points) + ) { + return; + } + + const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); + const center = pointFrom((x1 + x2) / 2, (y1 + y2) / 2); + const rays = options?.rays ?? 100; + const radius = Math.max(x2 - x1, y2 - y1) * 2; + const points: GlobalPoint[] = []; + + for (let i = 0; i < rays; i += 1) { + const angle = (i / rays) * Math.PI * 2; + const end = pointFrom( + center[0] + Math.cos(angle) * radius, + center[1] + Math.sin(angle) * radius, + ); + const hits = intersectElementWithLineSegment( + element, + elementsMap, + lineSegment(center, end), + ); + if (hits.length === 0) { + continue; + } + hits.sort(pointDistanceSq); + points.push(hits[0]); + } + + if (points.length >= 3) { + debugDrawPolygon(points, { + color: options?.color ?? "orange", + fill: options?.fill ?? true, + }); + } else { + console.warn( + `debugDrawHitVolume: could not compute hit volume for element ${element.id}`, + ); + } +}; + export const debugDrawCubicBezier = ( c: Curve, opts?: { @@ -61,6 +128,31 @@ export const debugDrawLine = ( ); }; +export const debugDrawPolygon = ( + points: GlobalPoint[], + opts?: { + color?: string; + permanent?: boolean; + fill?: boolean; + close?: boolean; + }, +) => { + if (points.length < 2) { + return; + } + + addToCurrentFrame({ + color: opts?.color ?? "orange", + permanent: !!opts?.permanent, + data: { + type: "polygon", + points, + fill: opts?.fill, + close: opts?.close, + }, + }); +}; + export const debugDrawPoint = ( p: GlobalPoint, opts?: { @@ -101,7 +193,7 @@ export const debugDrawBounds = ( permanent?: boolean; }, ) => { - (isBounds(box) ? [box] : box).forEach((bbox) => + (isBounds(box) ? [box] : box).forEach((bbox: Bounds) => debugDrawLine( [ lineSegment(