Skip to content

Commit

Permalink
feat(draw): improve empty text element hit interaction (#1002)
Browse files Browse the repository at this point in the history
  • Loading branch information
pubuzhixing8 authored Jan 3, 2025
1 parent 69b8602 commit 9594720
Show file tree
Hide file tree
Showing 14 changed files with 170 additions and 71 deletions.
17 changes: 17 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"@plait/angular-board": "0.74.0",
"@plait/angular-text": "0.74.0",
"@plait/common": "0.74.0",
"@plait/core": "0.74.0",
"@plait/draw": "0.74.0",
"@plait/flow": "0.74.0",
"@plait/graph-viz": "0.74.0",
"@plait/layouts": "0.74.0",
"@plait/mind": "0.74.0",
"@plait/text-plugins": "0.74.0"
},
"changesets": []
}
5 changes: 5 additions & 0 deletions .changeset/silver-kings-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plait/draw': minor
---

isHitDrawElement support isStrict mode to match dblClick editing scene(isStrict is false)
5 changes: 5 additions & 0 deletions .changeset/thick-apricots-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plait/core': minor
---

override isHit method support isStrict param
2 changes: 1 addition & 1 deletion packages/core/src/interfaces/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface PlaitBoard {
drawElement: (context: PlaitPluginElementContext) => ComponentType<ElementFlavour>;
isRectangleHit: (element: PlaitElement, range: Selection) => boolean;
// When the element has no fill color, it is considered a hit only if it hits the border.
isHit: (element: PlaitElement, point: Point) => boolean;
isHit: (element: PlaitElement, point: Point, isStrict?: boolean) => boolean;
isInsidePoint: (element: PlaitElement, point: Point) => boolean;
// the hit element is determined by the plugin
getOneHitElement: (hitElements: PlaitElement[]) => PlaitElement;
Expand Down
41 changes: 30 additions & 11 deletions packages/core/src/utils/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function getNearestPointBetweenPointAndEllipse(point: Point, center: Poin
const a = Math.abs(rectangleClient.width) / 2;
const b = Math.abs(rectangleClient.height) / 2;

[0, 1, 2, 3].forEach(x => {
[0, 1, 2, 3].forEach((x) => {
const xx = a * tx;
const yy = b * ty;

Expand Down Expand Up @@ -179,26 +179,45 @@ export const isLineHitLine = (a: Point, b: Point, c: Point, d: Point): boolean =
return crossProduct(ab, ac) * crossProduct(ab, ad) <= 0 && crossProduct(cd, ca) * crossProduct(cd, cb) <= 0;
};

export const isPolylineHitRectangle = (points: Point[], rectangle: RectangleClient, isClose: boolean = true) => {
export const isLineHitRectangle = (points: Point[], rectangle: RectangleClient, isClose: boolean = true) => {
const rectanglePoints = RectangleClient.getCornerPoints(rectangle);
const len = points.length;
for (let i = 0; i < len; i++) {
if (i === len - 1 && !isClose) continue;
const p1 = points[i];
const p2 = points[(i + 1) % len];
const isHit =
isLineHitLine(p1, p2, rectanglePoints[0], rectanglePoints[1]) ||
isLineHitLine(p1, p2, rectanglePoints[1], rectanglePoints[2]) ||
isLineHitLine(p1, p2, rectanglePoints[2], rectanglePoints[3]) ||
isLineHitLine(p1, p2, rectanglePoints[3], rectanglePoints[0]);
const isHit = isSingleLineHitRectangleEdge(p1, p2, rectangle);
if (isHit || isPointInPolygon(p1, rectanglePoints) || isPointInPolygon(p2, rectanglePoints)) {
return true;
}
}
return false;
};

export const isLineHitRectangleEdge = (points: Point[], rectangle: RectangleClient, isClose: boolean = true) => {
const len = points.length;
for (let i = 0; i < len; i++) {
if (i === len - 1 && !isClose) continue;
const p1 = points[i];
const p2 = points[(i + 1) % len];
const isHit = isSingleLineHitRectangleEdge(p1, p2, rectangle);
if (isHit) {
return true;
}
}
return false;
};

export const isSingleLineHitRectangleEdge = (p1: Point, p2: Point, rectangle: RectangleClient) => {
const rectanglePoints = RectangleClient.getCornerPoints(rectangle);
return (
isLineHitLine(p1, p2, rectanglePoints[0], rectanglePoints[1]) ||
isLineHitLine(p1, p2, rectanglePoints[1], rectanglePoints[2]) ||
isLineHitLine(p1, p2, rectanglePoints[2], rectanglePoints[3]) ||
isLineHitLine(p1, p2, rectanglePoints[3], rectanglePoints[0])
);
};

//https://stackoverflow.com/questions/22521982/check-if-point-is-inside-a-polygon
export const isPointInPolygon = (point: Point, points: Point[]) => {
// ray-casting algorithm based on
Expand Down Expand Up @@ -262,7 +281,7 @@ export const isPointInRoundRectangle = (point: Point, rectangle: RectangleClient
};

// https://gist.github.com/nicholaswmin/c2661eb11cad5671d816
export const catmullRomFitting = function(points: Point[]) {
export const catmullRomFitting = function (points: Point[]) {
const alpha = 0.5;
let p0, p1, p2, p3, bp1, bp2, d1, d2, d3, A, B, N, M;
var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
Expand Down Expand Up @@ -423,9 +442,9 @@ export function getCrossingPointsBetweenEllipseAndSegment(
return (
tValues
// Filter to only points that are on the segment.
.filter(t => !segment_only || (t >= 0 && t <= 1))
.filter((t) => !segment_only || (t >= 0 && t <= 1))
// Solve for points.
.map(t => [startPoint[0] + (endPoint[0] - startPoint[0]) * t + cx, startPoint[1] + (endPoint[1] - startPoint[1]) * t + cy])
.map((t) => [startPoint[0] + (endPoint[0] - startPoint[0]) * t + cx, startPoint[1] + (endPoint[1] - startPoint[1]) * t + cy])
);
}

Expand All @@ -439,4 +458,4 @@ export function getCrossingPointsBetweenEllipseAndSegment(
*/
export function getPointBetween(x0: number, y0: number, x1: number, y1: number, d = 0.5) {
return [x0 + (x1 - x0) * d, y0 + (y1 - y0) * d];
}
}
22 changes: 12 additions & 10 deletions packages/core/src/utils/selected-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const getHitElementsBySelection = (
}
depthFirstRecursion<Ancestor>(
board,
node => {
(node) => {
if (!PlaitBoard.isBoard(node) && match(node)) {
let isRectangleHit = false;
try {
Expand All @@ -57,18 +57,19 @@ export const getHitElementsBySelection = (
export const getHitElementsByPoint = (
board: PlaitBoard,
point: Point,
match: (element: PlaitElement) => boolean = () => true
match: (element: PlaitElement) => boolean = () => true,
isStrict = true
): PlaitElement[] => {
let hitElements: PlaitElement[] = [];
depthFirstRecursion<Ancestor>(
board,
node => {
(node) => {
if (PlaitBoard.isBoard(node) || !match(node) || !PlaitElement.hasMounted(node)) {
return;
}
let isHit = false;
try {
isHit = board.isHit(node, point);
isHit = board.isHit(node, point, isStrict);
} catch (error) {
if (isDebug()) {
console.error('isHit', error, 'node', node);
Expand All @@ -88,9 +89,10 @@ export const getHitElementsByPoint = (
export const getHitElementByPoint = (
board: PlaitBoard,
point: Point,
match: (element: PlaitElement) => boolean = () => true
match: (element: PlaitElement) => boolean = () => true,
isStrict = true
): undefined | PlaitElement => {
const pointHitElements = getHitElementsByPoint(board, point, match);
const pointHitElements = getHitElementsByPoint(board, point, match, isStrict);
const hitElement = board.getOneHitElement(pointHitElements);
return hitElement;
};
Expand Down Expand Up @@ -133,15 +135,15 @@ export const removeSelectedElement = (board: PlaitBoard, element: PlaitElement,
if (board.isRecursion(element) && isRemoveChildren) {
depthFirstRecursion(
element,
node => {
(node) => {
targetElements.push(node);
},
node => board.isRecursion(node)
(node) => board.isRecursion(node)
);
} else {
targetElements.push(element);
}
const newSelectedElements = selectedElements.filter(value => !targetElements.includes(value));
const newSelectedElements = selectedElements.filter((value) => !targetElements.includes(value));
cacheSelectedElements(board, newSelectedElements);
}
};
Expand All @@ -157,7 +159,7 @@ export const clearSelectedElement = (board: PlaitBoard) => {

export const isSelectedElement = (board: PlaitBoard, element: PlaitElement) => {
const selectedElements = getSelectedElements(board);
return !!selectedElements.find(value => value === element);
return !!selectedElements.find((value) => value === element);
};

export const temporaryDisableSelection = (board: PlaitOptionsBoard) => {
Expand Down
55 changes: 44 additions & 11 deletions packages/draw/src/engines/uml/provided-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,53 @@ import {
Point,
PointOfRectangle,
RectangleClient,
distanceBetweenPointAndPoint,
getEllipseTangentSlope,
getNearestPointBetweenPointAndEllipse,
getNearestPointBetweenPointAndSegments,
getVectorFromPointAndSlope,
setStrokeLinecap
} from '@plait/core';
import { ShapeEngine } from '../../interfaces';
import { Options } from 'roughjs/bin/core';
import { RectangleEngine } from '../basic-shapes/rectangle';
import { getUnitVectorByPointAndPoint } from '@plait/common';

const percentage = 0.54;

export const getStartPoint = (rectangle: RectangleClient): Point => {
return [rectangle.x, rectangle.y + rectangle.height / 2];
};

export const getEndPoint = (rectangle: RectangleClient): Point => {
return [rectangle.x + rectangle.width * percentage, rectangle.y + rectangle.height / 2];
};

export const arcPercentage = percentage + (1 - percentage) / 2;

export const getArcCenter = (rectangle: RectangleClient): Point => {
return [rectangle.x + arcPercentage * rectangle.width, rectangle.y + rectangle.height / 2];
};

export const ProvidedInterfaceEngine: ShapeEngine = {
draw(board: PlaitBoard, rectangle: RectangleClient, options: Options) {
const rs = PlaitBoard.getRoughSVG(board);
const startPoint = getStartPoint(rectangle);
const endPoint = getEndPoint(rectangle);
const shape = rs.path(
` M${rectangle.x} ${rectangle.y + rectangle.height / 2}
H${rectangle.x + rectangle.width * 0.54}
A${(rectangle.width * 0.46) / 2} ${rectangle.height / 2}, 0, 1, 1 ${rectangle.x + rectangle.width} ${rectangle.y +
rectangle.height / 2}
A${(rectangle.width * 0.46) / 2} ${rectangle.height / 2}, 0, 1, 1 ${rectangle.x + rectangle.width * 0.54} ${rectangle.y +
rectangle.height / 2}
`,
`M${startPoint[0]} ${startPoint[1]}
H${endPoint[0]}
A${(rectangle.width * (1 - percentage)) / 2} ${rectangle.height / 2}, 0, 1, 1 ${rectangle.x + rectangle.width} ${
rectangle.y + rectangle.height / 2
}
A${(rectangle.width * (1 - percentage)) / 2} ${rectangle.height / 2}, 0, 1, 1 ${rectangle.x + rectangle.width * percentage} ${
rectangle.y + rectangle.height / 2
}`,
{
...options,
fillStyle: 'solid'
}
);
setStrokeLinecap(shape, 'round');

return shape;
},
isInsidePoint(rectangle: RectangleClient, point: Point) {
Expand All @@ -44,8 +63,22 @@ export const ProvidedInterfaceEngine: ShapeEngine = {
return RectangleClient.getEdgeCenterPoints(rectangle);
},
getNearestPoint(rectangle: RectangleClient, point: Point) {
const nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle));
return nearestPoint;
const startPoint = getStartPoint(rectangle);
const endPoint = getEndPoint(rectangle);
const nearestPointForLine = getNearestPointBetweenPointAndSegments(point, [startPoint, endPoint]);
const distanceForLine = distanceBetweenPointAndPoint(...point, ...nearestPointForLine);
const arcCenter = getArcCenter(rectangle);
const nearestPointForEllipse = getNearestPointBetweenPointAndEllipse(
point,
arcCenter,
(rectangle.width * (1 - percentage)) / 2,
rectangle.height / 2
);
const distanceForEllipse = distanceBetweenPointAndPoint(...point, ...nearestPointForEllipse);
if (distanceForLine < distanceForEllipse) {
return nearestPointForLine;
}
return nearestPointForEllipse;
},
getTangentVectorByConnectionPoint(rectangle: RectangleClient, pointOfRectangle: PointOfRectangle) {
const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle);
Expand Down
8 changes: 4 additions & 4 deletions packages/draw/src/plugins/with-draw-hotkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PlaitBoard, getHitElementByPoint, getSelectedElements, toHostPoint, toV
import { isVirtualKey, isSpaceHotkey, isDelete } from '@plait/common';
import { GeometryCommonTextKeys, PlaitDrawElement } from '../interfaces';
import { editText } from '../utils/geometry';
import { getHitMultipleGeometryText, isMultipleTextGeometry } from '../utils';
import { getHitMultipleGeometryText, isDrawElementIncludeText, isMultipleTextGeometry } from '../utils';

export const withDrawHotkey = (board: PlaitBoard) => {
const { keyDown, dblClick } = board;
Expand Down Expand Up @@ -31,12 +31,12 @@ export const withDrawHotkey = (board: PlaitBoard) => {
event.preventDefault();
if (!PlaitBoard.isReadonly(board)) {
const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y));
const hitElement = getHitElementByPoint(board, point);
if (hitElement && PlaitDrawElement.isGeometry(hitElement)) {
const hitElement = getHitElementByPoint(board, point, undefined, false);
if (hitElement && PlaitDrawElement.isGeometry(hitElement) && isDrawElementIncludeText(hitElement)) {
if (isMultipleTextGeometry(hitElement)) {
const hitText =
getHitMultipleGeometryText(hitElement, point) ||
hitElement.texts.find(item => item.id.includes(GeometryCommonTextKeys.content)) ||
hitElement.texts.find((item) => item.id.includes(GeometryCommonTextKeys.content)) ||
hitElement.texts[0];
editText(board, hitElement, hitText);
} else {
Expand Down
6 changes: 3 additions & 3 deletions packages/draw/src/plugins/with-draw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ export const withDraw = (board: PlaitBoard) => {
return isRectangleHit(element, selection);
};

board.isHit = (element, point) => {
const result = isHitDrawElement(board, element, point);
board.isHit = (element, point, isStrict?: boolean) => {
const result = isHitDrawElement(board, element, point, isStrict);
if (result !== null) {
return result;
}
return isHit(element, point);
return isHit(element, point, isStrict);
};

board.getOneHitElement = elements => {
Expand Down
8 changes: 4 additions & 4 deletions packages/draw/src/plugins/with-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
PlaitElement,
RectangleClient,
Selection,
isPolylineHitRectangle,
isLineHitRectangle,
toViewBoxPoint,
toHostPoint,
getHitElementByPoint,
Expand All @@ -33,12 +33,12 @@ export const withTable = (board: PlaitBoard) => {
return drawElement(context);
};

tableBoard.isHit = (element, point) => {
tableBoard.isHit = (element, point, isStrict?: boolean) => {
if (PlaitDrawElement.isElementByTable(element)) {
const client = RectangleClient.getRectangleByPoints(element.points);
return RectangleClient.isPointInRectangle(client, point);
}
return isHit(element, point);
return isHit(element, point, isStrict);
};

tableBoard.getRectangle = (element: PlaitElement) => {
Expand All @@ -59,7 +59,7 @@ export const withTable = (board: PlaitBoard) => {
tableBoard.isRectangleHit = (element: PlaitElement, selection: Selection) => {
if (PlaitDrawElement.isElementByTable(element)) {
const rangeRectangle = RectangleClient.getRectangleByPoints([selection.anchor, selection.focus]);
return isPolylineHitRectangle(element.points, rangeRectangle);
return isLineHitRectangle(element.points, rangeRectangle);
}
return isRectangleHit(element, selection);
};
Expand Down
Loading

0 comments on commit 9594720

Please sign in to comment.