Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(draw): refactor getHitElementByPoint return value #WIK-15721 #922

Merged
merged 8 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/large-planes-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@plait/common': minor
'@plait/core': minor
'@plait/draw': minor
'@plait/mind': minor
---

refactor getHitElementByPoint return value

add getHitElement to board, the hit element is determined by the plugin


1 change: 1 addition & 0 deletions packages/common/src/constants/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export const TRANSPARENT = 'transparent';
export const ROTATE_HANDLE_DISTANCE_TO_ELEMENT = 20;
export const ROTATE_HANDLE_SIZE = 18;
export const DEFAULT_FONT_FAMILY = 'PingFangSC-Regular, "PingFang SC"';
export const DEFAULT_FILL = 'none';
22 changes: 22 additions & 0 deletions packages/common/src/utils/elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PlaitBoard, PlaitElement } from '@plait/core';
import { DEFAULT_FILL, TRANSPARENT } from '../constants';

export const getElementArea = (board: PlaitBoard, element: PlaitElement) => {
const rectangle = board.getRectangle(element);
if (rectangle) {
return rectangle.width * rectangle.height;
}
return 0;
};

export const sortElementsByArea = (board: PlaitBoard, elements: PlaitElement[], direction: 'desc' | 'asc' = 'asc') => {
return elements.sort((a, b) => {
const areaA = getElementArea(board, a);
const areaB = getElementArea(board, b);
return direction === 'asc' ? areaA - areaB : areaB - areaA;
});
};

export const isFilled = (fill: string) => {
return fill && fill !== DEFAULT_FILL && fill !== TRANSPARENT;
};
1 change: 1 addition & 0 deletions packages/common/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './vector';
export * from './math';
export * from './drawing';
export * from './rotate';
export * from './elements';
1 change: 1 addition & 0 deletions packages/core/src/interfaces/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface PlaitBoard {
// When the element has no fill color, it is considered a hit only if it hits the border.
isHit: (element: PlaitElement, point: Point) => boolean;
isInsidePoint: (element: PlaitElement, point: Point) => boolean;
getHitElement: (hitElements: PlaitElement[]) => PlaitElement;
isRecursion: (element: PlaitElement) => boolean;
isMovable: (element: PlaitElement) => boolean;
getRectangle: (element: PlaitElement) => RectangleClient | null;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/plugins/create-board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function createBoard(children: PlaitElement[], options?: PlaitBoardOption
isRectangleHit: element => false,
isHit: element => false,
isInsidePoint: element => false,
getHitElement: (data: PlaitElement[]) => data[0],
isRecursion: element => true,
isMovable: element => false,
getRectangle: element => null,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/plugins/with-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ export function withSelection(board: PlaitBoard) {
if (isHitElementWithGroup) {
setSelectedElementsWithGroup(board, elements, isShift);
} else {
if (board.selection && Selection.isCollapsed(board.selection)) {
const element = board.getHitElement(elements);
if (element) {
elements = [element];
}
}
if (isShift) {
const newElements = [...selectedElements];
if (board.selection && Selection.isCollapsed(board.selection)) {
Expand Down
42 changes: 18 additions & 24 deletions packages/core/src/utils/selected-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export const getHitElementsBySelection = (
}
const isCollapsed = Selection.isCollapsed(newSelection);
if (isCollapsed) {
const hitElement = getHitElementByPoint(board, newSelection.anchor, match);
if (hitElement) {
return [hitElement];
const hitElements = getHitElementsByPoint(board, newSelection.anchor, match);
if (hitElements?.length) {
return hitElements;
} else {
return [];
}
Expand All @@ -44,43 +44,37 @@ export const getHitElementsBySelection = (
return rectangleHitElements;
};

export const getHitElementByPoint = (
export const getHitElementsByPoint = (
board: PlaitBoard,
point: Point,
match: (element: PlaitElement) => boolean = () => true
): undefined | PlaitElement => {
let hitElement: PlaitElement | undefined = undefined;
let hitInsideElement: PlaitElement | undefined = undefined;
): PlaitElement[] => {
let hitElements: PlaitElement[] = [];
depthFirstRecursion<Ancestor>(
board,
node => {
if (hitElement) {
return;
}
if (PlaitBoard.isBoard(node) || !match(node) || !PlaitElement.hasMounted(node)) {
return;
}
if (board.isHit(node, point)) {
hitElement = node;
hitElements.push(node);
return;
}

/**
* 需要增加场景测试
* hitInsideElement 存的是第一个符合 isInsidePoint 的元素
* 当有元素符合 isHit 时结束遍历,并返回 hitElement
* 当所有元素都不符合 isHit ,则返回第一个符合 isInsidePoint 的元素
* 这样保证最上面的元素优先被探测到;
*/

if (!hitInsideElement && board.isInsidePoint(node, point)) {
hitInsideElement = node;
}
},
getIsRecursionFunc(board),
true
);
return hitElement || hitInsideElement;
return hitElements;
};

export const getHitElementByPoint = (
board: PlaitBoard,
point: Point,
match: (element: PlaitElement) => boolean = () => true
): undefined | PlaitElement => {
const pointHitElements = getHitElementsByPoint(board, point, match);
const hitElement = board.getHitElement(pointHitElements);
return hitElement;
};

export const getHitSelectedElements = (board: PlaitBoard, point: Point) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/draw/src/constants/geometry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ACTIVE_STROKE_WIDTH } from '@plait/core';
import { BasicShapes, FlowchartSymbols, GeometryShapes, MultipleTextGeometryCommonTextKeys, UMLSymbols } from '../interfaces';
import { Alignment } from '@plait/common';
import { Alignment, DEFAULT_FILL } from '@plait/common';

export const ShapeDefaultSpace = {
rectangleAndText: 4
Expand All @@ -10,7 +10,7 @@ export const DefaultDrawStyle = {
strokeWidth: 2,
defaultRadius: 4,
strokeColor: '#000',
fill: 'none'
fill: DEFAULT_FILL
};

export const DefaultDrawActiveStyle = {
Expand Down
9 changes: 7 additions & 2 deletions packages/draw/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ export const PlaitDrawElement = {
}
},
isShapeElement: (value: any): value is PlaitShapeElement => {
return PlaitDrawElement.isImage(value) || PlaitDrawElement.isGeometry(value) || PlaitDrawElement.isTable(value) || PlaitDrawElement.isSwimlane(value);
return (
PlaitDrawElement.isImage(value) ||
PlaitDrawElement.isGeometry(value) ||
PlaitDrawElement.isTable(value) ||
PlaitDrawElement.isSwimlane(value)
);
},
isBasicShape: (value: any) => {
return Object.keys(BasicShapes).includes(value.shape);
Expand Down Expand Up @@ -77,5 +82,5 @@ export const PlaitDrawElement = {
},
isElementByTable: (value: any): value is PlaitBaseTable => {
return PlaitDrawElement.isTable(value) || PlaitDrawElement.isSwimlane(value) || PlaitDrawElement.isGeometryByTable(value);
},
}
};
22 changes: 20 additions & 2 deletions packages/draw/src/plugins/with-draw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,24 @@ import { withLineAutoCompleteReaction } from './with-line-auto-complete-reaction
import { withLineAutoComplete } from './with-line-auto-complete';
import { withLineTextMove } from './with-line-text-move';
import { withDrawResize } from './with-draw-resize';
import { isHitDrawElement, isHitElementInside, isRectangleHitDrawElement } from '../utils/hit';
import { getDrawHitElement, isHitDrawElement, isHitElementInside, isRectangleHitDrawElement } from '../utils/hit';
import { getLinePoints, getLineTextRectangle } from '../utils/line/line-basic';
import { withDrawRotate } from './with-draw-rotate';
import { withTable } from './with-table';
import { withSwimlane } from './with-swimlane';

export const withDraw = (board: PlaitBoard) => {
const { drawElement, getRectangle, isRectangleHit, isHit, isInsidePoint, isMovable, isAlign, getRelatedFragment } = board;
const {
drawElement,
getRectangle,
isRectangleHit,
isHit,
isInsidePoint,
isMovable,
isAlign,
getRelatedFragment,
getHitElement
} = board;

board.drawElement = (context: PlaitPluginElementContext) => {
if (PlaitDrawElement.isGeometry(context.element)) {
Expand Down Expand Up @@ -73,6 +83,14 @@ export const withDraw = (board: PlaitBoard) => {
return isHit(element, point);
};

board.getHitElement = elements => {
const isDrawElements = elements.every(item => PlaitDrawElement.isDrawElement(item));
if (isDrawElements) {
return getDrawHitElement(board, elements as PlaitDrawElement[]);
}
return getHitElement(elements);
};

board.isInsidePoint = (element: PlaitElement, point: Point) => {
const result = isHitElementInside(board, element, point);
if (result !== null) {
Expand Down
91 changes: 58 additions & 33 deletions packages/draw/src/utils/hit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ import {
rotateAntiPointsByElement
} from '@plait/core';
import { PlaitCommonGeometry, PlaitDrawElement, PlaitGeometry, PlaitLine, PlaitShapeElement } from '../interfaces';
import { TRANSPARENT } from '@plait/common';
import { getNearestPoint } from './geometry';
import { getLinePoints } from './line/line-basic';
import { getFillByElement } from './style/stroke';
import { DefaultDrawStyle } from '../constants/geometry';
import { getEngine } from '../engines';
import { getElementShape } from './shape';
import { getHitLineTextIndex } from './position/line';
import { getTextRectangle } from './common';
import { isMultipleTextGeometry } from './multi-text-geometry';
import { isFilled, sortElementsByArea } from '@plait/common';

export const isTextExceedingBounds = (geometry: PlaitGeometry) => {
const client = RectangleClient.getRectangleByPoints(geometry.points);
Expand All @@ -46,35 +45,35 @@ export const isHitLine = (board: PlaitBoard, element: PlaitLine, point: Point) =
return isHitText || isHitPolyLine(points, point);
};

export const isHitElementText = (element: PlaitCommonGeometry, point: Point) => {
export const isRectangleHitElementText = (element: PlaitCommonGeometry, rectangle: RectangleClient) => {
const engine = getEngine<PlaitCommonGeometry>(element.shape);
if (isMultipleTextGeometry(element)) {
const texts = element.texts;
return texts.some(item => {
const textClient = engine.getTextRectangle!(element, { key: item.key });
return RectangleClient.isPointInRectangle(textClient, point);
const rotatedCornerPoints =
rotatePointsByElement(RectangleClient.getCornerPoints(textClient), element) || RectangleClient.getCornerPoints(textClient);
return isPolylineHitRectangle(rotatedCornerPoints, rectangle);
});
} else {
const textClient = engine.getTextRectangle ? engine.getTextRectangle(element) : getTextRectangle(element);
return RectangleClient.isPointInRectangle(textClient, point);
const rotatedCornerPoints =
rotatePointsByElement(RectangleClient.getCornerPoints(textClient), element) || RectangleClient.getCornerPoints(textClient);
return isPolylineHitRectangle(rotatedCornerPoints, rectangle);
}
};

export const isRectangleHitElementText = (element: PlaitCommonGeometry, rectangle: RectangleClient) => {
export const isHitElementText = (element: PlaitCommonGeometry, point: Point) => {
const engine = getEngine<PlaitCommonGeometry>(element.shape);
if (isMultipleTextGeometry(element)) {
const texts = element.texts;
return texts.some(item => {
const textClient = engine.getTextRectangle!(element, { key: item.key });
const rotatedCornerPoints =
rotatePointsByElement(RectangleClient.getCornerPoints(textClient), element) || RectangleClient.getCornerPoints(textClient);
return isPolylineHitRectangle(rotatedCornerPoints, rectangle);
return RectangleClient.isPointInRectangle(textClient, point);
});
} else {
const textClient = engine.getTextRectangle ? engine.getTextRectangle(element) : getTextRectangle(element);
const rotatedCornerPoints =
rotatePointsByElement(RectangleClient.getCornerPoints(textClient), element) || RectangleClient.getCornerPoints(textClient);
return isPolylineHitRectangle(rotatedCornerPoints, rectangle);
return RectangleClient.isPointInRectangle(textClient, point);
}
};

Expand Down Expand Up @@ -105,35 +104,62 @@ export const isRectangleHitDrawElement = (board: PlaitBoard, element: PlaitEleme
return null;
};

export const getDrawHitElement = (board: PlaitBoard, elements: PlaitDrawElement[]) => {
let firstFilledElement = getFirstFilledDrawElement(board, elements);
let endIndex = elements.length;
if (firstFilledElement) {
endIndex = elements.indexOf(firstFilledElement) + 1;
}
const newElements = elements.slice(0, endIndex);
const element = getFirstTextOrLineElement(newElements);
if (element) {
return element;
}
const sortElements = sortElementsByArea(board, newElements, 'asc');
return sortElements[0];
};

export const getFirstFilledDrawElement = (board: PlaitBoard, elements: PlaitDrawElement[]) => {
let filledElement: PlaitGeometry | null = null;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (PlaitDrawElement.isGeometry(element) && !PlaitDrawElement.isText(element)) {
const fill = getFillByElement(board, element);
if (isFilled(fill)) {
filledElement = element as PlaitGeometry;
break;
}
}
}
return filledElement;
};

export const getFirstTextOrLineElement = (elements: PlaitDrawElement[]) => {
const texts = elements.filter(item => PlaitDrawElement.isText(item));
if (texts.length) {
return texts[0];
}
const lines = elements.filter(item => PlaitDrawElement.isLine(item));
if (lines.length) {
return lines[0];
}
return null;
};

export const isHitDrawElement = (board: PlaitBoard, element: PlaitElement, point: Point) => {
const rectangle = board.getRectangle(element);
point = rotateAntiPointsByElement(point, element) || point;
if (PlaitDrawElement.isGeometry(element)) {
const fill = getFillByElement(board, element);
if (isHitEdgeOfShape(board, element, point, HIT_DISTANCE_BUFFER)) {
return true;
}
const engine = getEngine(getElementShape(element));
// when shape equals text, fill is not allowed
if (fill !== DefaultDrawStyle.fill && fill !== TRANSPARENT && !PlaitDrawElement.isText(element)) {
const isHitInside = engine.isInsidePoint(rectangle!, point);
if (isHitInside) {
return isHitInside;
}
} else {
// if shape equals text, only check text rectangle
if (PlaitDrawElement.isText(element)) {
const textClient = getTextRectangle(element);
let isHitText = RectangleClient.isPointInRectangle(textClient, point);
return isHitText;
}

// check textRectangle of element
const isHitText = isHitElementText(element, point);
huanhuanwa marked this conversation as resolved.
Show resolved Hide resolved
if (isHitText) {
return isHitText;
}
if (PlaitDrawElement.isText(element)) {
const textClient = getTextRectangle(element);
return RectangleClient.isPointInRectangle(textClient, point);
}
const isHitText = isHitElementText(element, point);
return isHitText || engine.isInsidePoint(rectangle!, point);
}
if (PlaitDrawElement.isImage(element)) {
const client = RectangleClient.getRectangleByPoints(element.points);
Expand Down Expand Up @@ -165,7 +191,6 @@ export const isHitElementInside = (board: PlaitBoard, element: PlaitElement, poi
if (isHitInside) {
return isHitInside;
}

if (engine.getTextRectangle) {
huanhuanwa marked this conversation as resolved.
Show resolved Hide resolved
const isHitText = isHitElementText(element, point);
if (isHitText) {
Expand Down
Loading
Loading