mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 19:04:35 +01:00 
			
		
		
		
	 79d9dc2f8f
			
		
	
	79d9dc2f8f
	
	
	
		
			
			* fix: make bounds independent of scene * pass only elements to getCommonBounds * lint * pass elementsMap to getVisibleAndNonSelectedElements
		
			
				
	
	
		
			212 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			212 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import type {
 | |
|   ExcalidrawElement,
 | |
|   ExcalidrawFreeDrawElement,
 | |
|   ExcalidrawLinearElement,
 | |
|   NonDeletedExcalidrawElement,
 | |
| } from "../excalidraw/element/types";
 | |
| import {
 | |
|   isArrowElement,
 | |
|   isExcalidrawElement,
 | |
|   isFreeDrawElement,
 | |
|   isLinearElement,
 | |
|   isTextElement,
 | |
| } from "../excalidraw/element/typeChecks";
 | |
| import { isValueInRange, rotatePoint } from "../excalidraw/math";
 | |
| import type { Point } from "../excalidraw/types";
 | |
| import { Bounds, getElementBounds } from "../excalidraw/element/bounds";
 | |
| import { arrayToMap } from "../excalidraw/utils";
 | |
| 
 | |
| type Element = NonDeletedExcalidrawElement;
 | |
| type Elements = readonly NonDeletedExcalidrawElement[];
 | |
| 
 | |
| type Points = readonly Point[];
 | |
| 
 | |
| /** @returns vertices relative to element's top-left [0,0] position  */
 | |
| const getNonLinearElementRelativePoints = (
 | |
|   element: Exclude<
 | |
|     Element,
 | |
|     ExcalidrawLinearElement | ExcalidrawFreeDrawElement
 | |
|   >,
 | |
| ): [TopLeft: Point, TopRight: Point, BottomRight: Point, BottomLeft: Point] => {
 | |
|   if (element.type === "diamond") {
 | |
|     return [
 | |
|       [element.width / 2, 0],
 | |
|       [element.width, element.height / 2],
 | |
|       [element.width / 2, element.height],
 | |
|       [0, element.height / 2],
 | |
|     ];
 | |
|   }
 | |
|   return [
 | |
|     [0, 0],
 | |
|     [0 + element.width, 0],
 | |
|     [0 + element.width, element.height],
 | |
|     [0, element.height],
 | |
|   ];
 | |
| };
 | |
| 
 | |
| /** @returns vertices relative to element's top-left [0,0] position  */
 | |
| const getElementRelativePoints = (element: ExcalidrawElement): Points => {
 | |
|   if (isLinearElement(element) || isFreeDrawElement(element)) {
 | |
|     return element.points;
 | |
|   }
 | |
|   return getNonLinearElementRelativePoints(element);
 | |
| };
 | |
| 
 | |
| const getMinMaxPoints = (points: Points) => {
 | |
|   const ret = points.reduce(
 | |
|     (limits, [x, y]) => {
 | |
|       limits.minY = Math.min(limits.minY, y);
 | |
|       limits.minX = Math.min(limits.minX, x);
 | |
| 
 | |
|       limits.maxX = Math.max(limits.maxX, x);
 | |
|       limits.maxY = Math.max(limits.maxY, y);
 | |
| 
 | |
|       return limits;
 | |
|     },
 | |
|     {
 | |
|       minX: Infinity,
 | |
|       minY: Infinity,
 | |
|       maxX: -Infinity,
 | |
|       maxY: -Infinity,
 | |
|       cx: 0,
 | |
|       cy: 0,
 | |
|     },
 | |
|   );
 | |
| 
 | |
|   ret.cx = (ret.maxX + ret.minX) / 2;
 | |
|   ret.cy = (ret.maxY + ret.minY) / 2;
 | |
| 
 | |
|   return ret;
 | |
| };
 | |
| 
 | |
| const getRotatedBBox = (element: Element): Bounds => {
 | |
|   const points = getElementRelativePoints(element);
 | |
| 
 | |
|   const { cx, cy } = getMinMaxPoints(points);
 | |
|   const centerPoint: Point = [cx, cy];
 | |
| 
 | |
|   const rotatedPoints = points.map((point) =>
 | |
|     rotatePoint([point[0], point[1]], centerPoint, element.angle),
 | |
|   );
 | |
|   const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints);
 | |
| 
 | |
|   return [
 | |
|     minX + element.x,
 | |
|     minY + element.y,
 | |
|     maxX + element.x,
 | |
|     maxY + element.y,
 | |
|   ];
 | |
| };
 | |
| 
 | |
| export const isElementInsideBBox = (
 | |
|   element: Element,
 | |
|   bbox: Bounds,
 | |
|   eitherDirection = false,
 | |
| ): boolean => {
 | |
|   const elementBBox = getRotatedBBox(element);
 | |
| 
 | |
|   const elementInsideBbox =
 | |
|     bbox[0] <= elementBBox[0] &&
 | |
|     bbox[2] >= elementBBox[2] &&
 | |
|     bbox[1] <= elementBBox[1] &&
 | |
|     bbox[3] >= elementBBox[3];
 | |
| 
 | |
|   if (!eitherDirection) {
 | |
|     return elementInsideBbox;
 | |
|   }
 | |
| 
 | |
|   if (elementInsideBbox) {
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     elementBBox[0] <= bbox[0] &&
 | |
|     elementBBox[2] >= bbox[2] &&
 | |
|     elementBBox[1] <= bbox[1] &&
 | |
|     elementBBox[3] >= bbox[3]
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const elementPartiallyOverlapsWithOrContainsBBox = (
 | |
|   element: Element,
 | |
|   bbox: Bounds,
 | |
| ): boolean => {
 | |
|   const elementBBox = getRotatedBBox(element);
 | |
| 
 | |
|   return (
 | |
|     (isValueInRange(elementBBox[0], bbox[0], bbox[2]) ||
 | |
|       isValueInRange(bbox[0], elementBBox[0], elementBBox[2])) &&
 | |
|     (isValueInRange(elementBBox[1], bbox[1], bbox[3]) ||
 | |
|       isValueInRange(bbox[1], elementBBox[1], elementBBox[3]))
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const elementsOverlappingBBox = ({
 | |
|   elements,
 | |
|   bounds,
 | |
|   type,
 | |
|   errorMargin = 0,
 | |
| }: {
 | |
|   elements: Elements;
 | |
|   bounds: Bounds | ExcalidrawElement;
 | |
|   /** safety offset. Defaults to 0. */
 | |
|   errorMargin?: number;
 | |
|   /**
 | |
|    * - overlap: elements overlapping or inside bounds
 | |
|    * - contain: elements inside bounds or bounds inside elements
 | |
|    * - inside: elements inside bounds
 | |
|    **/
 | |
|   type: "overlap" | "contain" | "inside";
 | |
| }) => {
 | |
|   if (isExcalidrawElement(bounds)) {
 | |
|     bounds = getElementBounds(bounds, arrayToMap(elements));
 | |
|   }
 | |
|   const adjustedBBox: Bounds = [
 | |
|     bounds[0] - errorMargin,
 | |
|     bounds[1] - errorMargin,
 | |
|     bounds[2] + errorMargin,
 | |
|     bounds[3] + errorMargin,
 | |
|   ];
 | |
| 
 | |
|   const includedElementSet = new Set<string>();
 | |
| 
 | |
|   for (const element of elements) {
 | |
|     if (includedElementSet.has(element.id)) {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     const isOverlaping =
 | |
|       type === "overlap"
 | |
|         ? elementPartiallyOverlapsWithOrContainsBBox(element, adjustedBBox)
 | |
|         : type === "inside"
 | |
|         ? isElementInsideBBox(element, adjustedBBox)
 | |
|         : isElementInsideBBox(element, adjustedBBox, true);
 | |
| 
 | |
|     if (isOverlaping) {
 | |
|       includedElementSet.add(element.id);
 | |
| 
 | |
|       if (element.boundElements) {
 | |
|         for (const boundElement of element.boundElements) {
 | |
|           includedElementSet.add(boundElement.id);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (isTextElement(element) && element.containerId) {
 | |
|         includedElementSet.add(element.containerId);
 | |
|       }
 | |
| 
 | |
|       if (isArrowElement(element)) {
 | |
|         if (element.startBinding) {
 | |
|           includedElementSet.add(element.startBinding.elementId);
 | |
|         }
 | |
| 
 | |
|         if (element.endBinding) {
 | |
|           includedElementSet.add(element.endBinding?.elementId);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return elements.filter((element) => includedElementSet.has(element.id));
 | |
| };
 |