feat: add approximate elements in bbox detection
This commit is contained in:
parent
3d57112480
commit
e8f1e10f36
@ -437,7 +437,7 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
const getBoundsFromPoints = (
|
||||
export const getBoundsFromPoints = (
|
||||
points: ExcalidrawFreeDrawElement["points"],
|
||||
): [number, number, number, number] => {
|
||||
let minX = Infinity;
|
||||
|
127
src/frame.ts
127
src/frame.ts
@ -23,6 +23,7 @@ import { moveOneRight } from "./zindex";
|
||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||
import { getElementLineSegments } from "./element/bounds";
|
||||
import { doLineSegmentsIntersect } from "./packages/utils";
|
||||
|
||||
// --------------------------- Frame State ------------------------------------
|
||||
export const bindElementsToFramesAfterDuplication = (
|
||||
@ -56,115 +57,7 @@ export const bindElementsToFramesAfterDuplication = (
|
||||
}
|
||||
};
|
||||
|
||||
// --------------------------- Frame Geometry ---------------------------------
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
class LineSegment {
|
||||
first: Point;
|
||||
second: Point;
|
||||
|
||||
constructor(pointA: Point, pointB: Point) {
|
||||
this.first = pointA;
|
||||
this.second = pointB;
|
||||
}
|
||||
|
||||
public getBoundingBox(): [Point, Point] {
|
||||
return [
|
||||
new Point(
|
||||
Math.min(this.first.x, this.second.x),
|
||||
Math.min(this.first.y, this.second.y),
|
||||
),
|
||||
new Point(
|
||||
Math.max(this.first.x, this.second.x),
|
||||
Math.max(this.first.y, this.second.y),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
||||
class FrameGeometry {
|
||||
private static EPSILON = 0.000001;
|
||||
|
||||
private static crossProduct(a: Point, b: Point) {
|
||||
return a.x * b.y - b.x * a.y;
|
||||
}
|
||||
|
||||
private static doBoundingBoxesIntersect(
|
||||
a: [Point, Point],
|
||||
b: [Point, Point],
|
||||
) {
|
||||
return (
|
||||
a[0].x <= b[1].x &&
|
||||
a[1].x >= b[0].x &&
|
||||
a[0].y <= b[1].y &&
|
||||
a[1].y >= b[0].y
|
||||
);
|
||||
}
|
||||
|
||||
private static isPointOnLine(a: LineSegment, b: Point) {
|
||||
const aTmp = new LineSegment(
|
||||
new Point(0, 0),
|
||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
||||
);
|
||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
||||
const r = this.crossProduct(aTmp.second, bTmp);
|
||||
return Math.abs(r) < this.EPSILON;
|
||||
}
|
||||
|
||||
private static isPointRightOfLine(a: LineSegment, b: Point) {
|
||||
const aTmp = new LineSegment(
|
||||
new Point(0, 0),
|
||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
||||
);
|
||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
||||
return this.crossProduct(aTmp.second, bTmp) < 0;
|
||||
}
|
||||
|
||||
private static lineSegmentTouchesOrCrossesLine(
|
||||
a: LineSegment,
|
||||
b: LineSegment,
|
||||
) {
|
||||
return (
|
||||
this.isPointOnLine(a, b.first) ||
|
||||
this.isPointOnLine(a, b.second) ||
|
||||
(this.isPointRightOfLine(a, b.first)
|
||||
? !this.isPointRightOfLine(a, b.second)
|
||||
: this.isPointRightOfLine(a, b.second))
|
||||
);
|
||||
}
|
||||
|
||||
private static doLineSegmentsIntersect(
|
||||
a: [readonly [number, number], readonly [number, number]],
|
||||
b: [readonly [number, number], readonly [number, number]],
|
||||
) {
|
||||
const aSegment = new LineSegment(
|
||||
new Point(a[0][0], a[0][1]),
|
||||
new Point(a[1][0], a[1][1]),
|
||||
);
|
||||
const bSegment = new LineSegment(
|
||||
new Point(b[0][0], b[0][1]),
|
||||
new Point(b[1][0], b[1][1]),
|
||||
);
|
||||
|
||||
const box1 = aSegment.getBoundingBox();
|
||||
const box2 = bSegment.getBoundingBox();
|
||||
return (
|
||||
this.doBoundingBoxesIntersect(box1, box2) &&
|
||||
this.lineSegmentTouchesOrCrossesLine(aSegment, bSegment) &&
|
||||
this.lineSegmentTouchesOrCrossesLine(bSegment, aSegment)
|
||||
);
|
||||
}
|
||||
|
||||
public static isElementIntersectingFrame(
|
||||
export function isElementIntersectingFrame(
|
||||
element: ExcalidrawElement,
|
||||
frame: ExcalidrawFrameElement,
|
||||
) {
|
||||
@ -174,13 +67,12 @@ class FrameGeometry {
|
||||
|
||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||
elementLineSegments.some((elementLineSegment) =>
|
||||
this.doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
||||
doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
||||
),
|
||||
);
|
||||
|
||||
return intersecting;
|
||||
}
|
||||
}
|
||||
|
||||
export const getElementsCompletelyInFrame = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -207,10 +99,7 @@ export const isElementContainingFrame = (
|
||||
export const getElementsIntersectingFrame = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameElement,
|
||||
) =>
|
||||
elements.filter((element) =>
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
||||
);
|
||||
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
|
||||
|
||||
export const elementsAreInFrameBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -236,7 +125,7 @@ export const elementOverlapsWithFrame = (
|
||||
) => {
|
||||
return (
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame) ||
|
||||
isElementIntersectingFrame(element, frame) ||
|
||||
isElementContainingFrame([frame], element, frame)
|
||||
);
|
||||
};
|
||||
@ -273,7 +162,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
|
||||
return !!elementsInGroup.find(
|
||||
(element) =>
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
||||
isElementIntersectingFrame(element, frame),
|
||||
);
|
||||
};
|
||||
|
||||
@ -294,7 +183,7 @@ export const groupsAreCompletelyOutOfFrame = (
|
||||
elementsInGroup.find(
|
||||
(element) =>
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
||||
isElementIntersectingFrame(element, frame),
|
||||
) === undefined
|
||||
);
|
||||
};
|
||||
@ -354,7 +243,7 @@ export const getElementsInResizingFrame = (
|
||||
);
|
||||
|
||||
for (const element of elementsNotCompletelyInFrame) {
|
||||
if (!FrameGeometry.isElementIntersectingFrame(element, frame)) {
|
||||
if (!isElementIntersectingFrame(element, frame)) {
|
||||
if (element.groupIds.length === 0) {
|
||||
nextElementsInFrame.delete(element);
|
||||
}
|
||||
|
72
src/packages/bbox.ts
Normal file
72
src/packages/bbox.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Point } from "../types";
|
||||
|
||||
export type LineSegment = [Point, Point];
|
||||
export type BBox = [topLeft: Point, bottomRight: Point];
|
||||
|
||||
export function bbox(topLeft: Point, bottomRight: Point): BBox {
|
||||
return [topLeft, bottomRight];
|
||||
}
|
||||
|
||||
export function getBBox(line: LineSegment): BBox {
|
||||
return [
|
||||
[Math.min(line[0][0], line[1][0]), Math.min(line[0][1], line[1][1])],
|
||||
[Math.max(line[0][0], line[1][0]), Math.max(line[0][1], line[1][1])],
|
||||
];
|
||||
}
|
||||
|
||||
export function crossProduct(a: Point, b: Point) {
|
||||
return a[0] * b[1] - b[0] * a[1];
|
||||
}
|
||||
|
||||
export function doBBoxesIntersect(a: BBox, b: BBox) {
|
||||
return (
|
||||
a[0][0] <= b[1][0] &&
|
||||
a[1][0] >= b[0][0] &&
|
||||
a[0][1] <= b[1][1] &&
|
||||
a[1][1] >= b[0][1]
|
||||
);
|
||||
}
|
||||
|
||||
export function translate(a: Point, b: Point): Point {
|
||||
return [a[0] - b[0], a[1] - b[1]];
|
||||
}
|
||||
|
||||
const EPSILON = 0.000001;
|
||||
|
||||
export function isPointOnLine(l: LineSegment, p: Point) {
|
||||
const p1 = translate(l[1], l[0]);
|
||||
const p2 = translate(p, l[0]);
|
||||
|
||||
const r = crossProduct(p1, p2);
|
||||
|
||||
return Math.abs(r) < EPSILON;
|
||||
}
|
||||
|
||||
export function isPointRightOfLine(l: LineSegment, p: Point) {
|
||||
const p1 = translate(l[1], l[0]);
|
||||
const p2 = translate(p, l[0]);
|
||||
|
||||
return crossProduct(p1, p2) < 0;
|
||||
}
|
||||
|
||||
export function isLineSegmentTouchingOrCrossingLine(
|
||||
a: LineSegment,
|
||||
b: LineSegment,
|
||||
) {
|
||||
return (
|
||||
isPointOnLine(a, b[0]) ||
|
||||
isPointOnLine(a, b[1]) ||
|
||||
(isPointRightOfLine(a, b[0])
|
||||
? !isPointRightOfLine(a, b[1])
|
||||
: isPointRightOfLine(a, b[1]))
|
||||
);
|
||||
}
|
||||
|
||||
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
||||
export function doLineSegmentsIntersect(a: LineSegment, b: LineSegment) {
|
||||
return (
|
||||
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
|
||||
isLineSegmentTouchingOrCrossingLine(a, b) &&
|
||||
isLineSegmentTouchingOrCrossingLine(b, a)
|
||||
);
|
||||
}
|
@ -226,6 +226,11 @@ export const exportToClipboard = async (
|
||||
}
|
||||
};
|
||||
|
||||
export * from "./bbox";
|
||||
export {
|
||||
elementsOverlappingBBox,
|
||||
isElementOverlappingBBox,
|
||||
} from "./withinBounds";
|
||||
export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json";
|
||||
export {
|
||||
loadFromBlob,
|
||||
|
224
src/packages/withinBounds.ts
Normal file
224
src/packages/withinBounds.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import { BBox, bbox } from "./bbox";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
isArrowElement,
|
||||
isFreeDrawElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "../element/typeChecks";
|
||||
import { getBoundsFromPoints } from "../element/bounds";
|
||||
import { rotatePoint } from "../math";
|
||||
import { Point } from "../types";
|
||||
|
||||
type Element = NonDeletedExcalidrawElement;
|
||||
type Elements = readonly NonDeletedExcalidrawElement[];
|
||||
|
||||
function getVertices(bbox: BBox): [Point, Point, Point, Point] {
|
||||
return [bbox[0], [bbox[1][0], bbox[0][1]], bbox[1], [bbox[0][0], bbox[1][1]]];
|
||||
}
|
||||
|
||||
function getPrimitiveBBox(element: Element): BBox {
|
||||
if (isFreeDrawElement(element)) {
|
||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(element.points);
|
||||
|
||||
return bbox(
|
||||
[minX + element.x, minY + element.y],
|
||||
[maxX + element.x, maxY + element.y],
|
||||
);
|
||||
} else if (isLinearElement(element)) {
|
||||
const { minX, minY, maxX, maxY } = element.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 },
|
||||
);
|
||||
|
||||
return bbox(
|
||||
[element.x + minX, element.y + minY],
|
||||
[element.x + maxX, element.y + maxY],
|
||||
);
|
||||
}
|
||||
|
||||
return bbox(
|
||||
[element.x, element.y],
|
||||
[element.x + element.width, element.y + element.height],
|
||||
);
|
||||
}
|
||||
|
||||
function getElementBBox(element: Element): BBox {
|
||||
const primitiveBBox = getPrimitiveBBox(element);
|
||||
|
||||
const centerPoint: Point = [
|
||||
primitiveBBox[0][0] + (primitiveBBox[1][0] - primitiveBBox[0][0]) / 2,
|
||||
primitiveBBox[0][1] + (primitiveBBox[1][1] - primitiveBBox[0][1]) / 2,
|
||||
];
|
||||
|
||||
const [otl, otr, obr, obl] = getVertices(primitiveBBox);
|
||||
|
||||
const rtl = rotatePoint(otl, centerPoint, element.angle);
|
||||
const rbr = rotatePoint(obr, centerPoint, element.angle);
|
||||
const rtr = rotatePoint(otr, centerPoint, element.angle);
|
||||
const rbl = rotatePoint(obl, centerPoint, element.angle);
|
||||
|
||||
return bbox(
|
||||
[
|
||||
Math.min(rtl[0], rbr[0], rtr[0], rbl[0]),
|
||||
Math.min(rtl[1], rbr[1], rtr[1], rbl[1]),
|
||||
],
|
||||
[
|
||||
Math.max(rtl[0], rbr[0], rtr[0], rbl[0]),
|
||||
Math.max(rtl[1], rbr[1], rtr[1], rbl[1]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function isElementInsideBBox(element: Element, bbox: BBox): boolean {
|
||||
const elementBBox = getElementBBox(element);
|
||||
|
||||
return (
|
||||
bbox[0][0] < elementBBox[0][0] &&
|
||||
bbox[1][0] > elementBBox[1][0] &&
|
||||
bbox[0][1] < elementBBox[0][1] &&
|
||||
bbox[1][1] > elementBBox[1][1]
|
||||
);
|
||||
}
|
||||
|
||||
function isValueInRange(value: number, min: number, max: number) {
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
|
||||
function isElementIntersectingBBox(element: Element, bbox: BBox): boolean {
|
||||
const elementBBox = getElementBBox(element);
|
||||
|
||||
return (
|
||||
bbox[0][0] < elementBBox[0][0] &&
|
||||
bbox[1][0] > elementBBox[1][0] &&
|
||||
bbox[0][1] < elementBBox[0][1] &&
|
||||
bbox[1][1] > elementBBox[1][1]
|
||||
);
|
||||
}
|
||||
|
||||
function isElementContainingBBox(element: Element, bbox: BBox): boolean {
|
||||
const elementBBox = getElementBBox(element);
|
||||
|
||||
return (
|
||||
(isValueInRange(elementBBox[0][0], bbox[0][0], bbox[1][0]) ||
|
||||
isValueInRange(bbox[0][0], elementBBox[0][0], elementBBox[1][0])) &&
|
||||
(isValueInRange(elementBBox[0][1], bbox[0][1], bbox[1][1]) ||
|
||||
isValueInRange(bbox[0][1], elementBBox[0][1], elementBBox[1][1]))
|
||||
);
|
||||
}
|
||||
|
||||
export function isElementOverlappingBBox(element: Element, bbox: BBox) {
|
||||
return (
|
||||
isElementInsideBBox(element, bbox) ||
|
||||
isElementIntersectingBBox(element, bbox) ||
|
||||
isElementContainingBBox(element, bbox)
|
||||
);
|
||||
}
|
||||
|
||||
export const elementsOverlappingBBox = ({
|
||||
elements,
|
||||
bounds,
|
||||
errorMargin = 0,
|
||||
}: {
|
||||
elements: Elements;
|
||||
bounds: BBox;
|
||||
errorMargin: number;
|
||||
}) => {
|
||||
const adjustedBBox = bbox(
|
||||
[bounds[0][0] - errorMargin, bounds[0][1] - errorMargin],
|
||||
[bounds[1][0] + errorMargin, bounds[1][1] + errorMargin],
|
||||
);
|
||||
|
||||
const includedElementSet = new Set<string>();
|
||||
|
||||
for (const element of elements) {
|
||||
if (includedElementSet.has(element.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isOverlaping = isElementOverlappingBBox(element, adjustedBBox);
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
// ### DEBUG
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
debug: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
window.debug = () => {
|
||||
const boundsIndex = window.h.elements.findIndex(
|
||||
(e) => e.type === "rectangle" && e.strokeStyle === "dashed",
|
||||
);
|
||||
|
||||
if (boundsIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const boundsElement = window.h.elements[boundsIndex];
|
||||
|
||||
const boundsBBox = bbox(
|
||||
[boundsElement.x, boundsElement.y],
|
||||
[
|
||||
boundsElement.x + boundsElement.width,
|
||||
boundsElement.y + boundsElement.height,
|
||||
],
|
||||
);
|
||||
|
||||
const allElements = [
|
||||
...window.h.elements.slice(0, boundsIndex),
|
||||
...window.h.elements.slice(boundsIndex + 1, window.h.elements.length),
|
||||
];
|
||||
|
||||
const boundedElements = elementsOverlappingBBox({
|
||||
elements: allElements,
|
||||
bounds: boundsBBox,
|
||||
errorMargin: 0,
|
||||
}).map((element) => element.id);
|
||||
|
||||
const newElements = allElements.map((element) => {
|
||||
if (boundedElements.includes(element.id)) {
|
||||
return { ...element, strokeColor: "#ff0000" };
|
||||
}
|
||||
|
||||
return { ...element, strokeColor: "#000000" };
|
||||
});
|
||||
|
||||
window.h.elements = [boundsElement, ...newElements];
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user