Skip to content

Commit

Permalink
refactor(actor): improve actor getNearestPoint logic #1016
Browse files Browse the repository at this point in the history
  • Loading branch information
pubuzhixing8 committed Feb 11, 2025
1 parent f64dbd7 commit fb47704
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 78 deletions.
65 changes: 34 additions & 31 deletions packages/core/src/utils/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function getNearestPointBetweenPointAndSegment(point: Point, linePoints:
return [xx, yy] as Point;
}

export function distanceBetweenPointAndSegments(points: Point[], point: Point) {
export function distanceBetweenPointAndSegments(point: Point, points: Point[]) {
const len = points.length;
let distance = Infinity;
if (points.length === 1) {
Expand Down Expand Up @@ -103,6 +103,23 @@ export function getNearestPointBetweenPointAndSegments(point: Point, points: Poi
return result;
}

export function getNearestPointBetweenPointAndDiscreteSegments(point: Point, segments: [Point, Point][]): Point {
let minDistance = Infinity;
let nearestPoint = point;

for (const segment of segments) {
const currentNearestPoint = getNearestPointBetweenPointAndSegment(point, segment);
const currentDistance = distanceBetweenPointAndPoint(point[0], point[1], currentNearestPoint[0], currentNearestPoint[1]);

if (currentDistance < minDistance) {
minDistance = currentDistance;
nearestPoint = currentNearestPoint;
}
}

return nearestPoint;
}

export function getNearestPointBetweenPointAndEllipse(point: Point, center: Point, rx: number, ry: number): Point {
const rectangleClient = {
x: center[0] - rx,
Expand Down Expand Up @@ -479,43 +496,38 @@ export function getPointBetween(x0: number, y0: number, x1: number, y1: number,
/**
* 计算椭圆弧的中心点和实际半径
*/
export function getEllipseArcCenter(
startPoint: Point,
arcCommand: SVGArcCommand
): { center: Point; rx: number; ry: number } {
export function getEllipseArcCenter(startPoint: Point, arcCommand: SVGArcCommand): { center: Point; rx: number; ry: number } {
// 1. 将坐标转换到标准位置
const dx = (arcCommand.endX - startPoint[0]) / 2;
const dy = (arcCommand.endY - startPoint[1]) / 2;
const cosAngle = Math.cos(arcCommand.xAxisRotation);
const sinAngle = Math.sin(arcCommand.xAxisRotation);

// 旋转到椭圆坐标系
const x1 = cosAngle * dx + sinAngle * dy;
const y1 = -sinAngle * dx + cosAngle * dy;

// 2. 计算中心点
const rx = Math.abs(arcCommand.rx);
const ry = Math.abs(arcCommand.ry);

// 确保半径足够大
const lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry);
const factor = lambda > 1 ? Math.sqrt(lambda) : 1;

const adjustedRx = rx * factor;
const adjustedRy = ry * factor;

// 计算中心点坐标
const sign = arcCommand.largeArcFlag === arcCommand.sweepFlag ? -1 : 1;
const sq = ((adjustedRx * adjustedRx * adjustedRy * adjustedRy) -
(adjustedRx * adjustedRx * y1 * y1) -
(adjustedRy * adjustedRy * x1 * x1)) /
((adjustedRx * adjustedRx * y1 * y1) +
(adjustedRy * adjustedRy * x1 * x1));
const sq =
(adjustedRx * adjustedRx * adjustedRy * adjustedRy - adjustedRx * adjustedRx * y1 * y1 - adjustedRy * adjustedRy * x1 * x1) /
(adjustedRx * adjustedRx * y1 * y1 + adjustedRy * adjustedRy * x1 * x1);
const coef = sign * Math.sqrt(Math.max(0, sq));

const centerX = coef * ((adjustedRx * y1) / adjustedRy);
const centerY = coef * (-(adjustedRy * x1) / adjustedRx);

// 3. 转换回原始坐标系
const cx = cosAngle * centerX - sinAngle * centerY + (startPoint[0] + arcCommand.endX) / 2;
const cy = sinAngle * centerX + cosAngle * centerY + (startPoint[1] + arcCommand.endY) / 2;
Expand All @@ -527,20 +539,11 @@ export function getEllipseArcCenter(
};
}

export function getNearestPointBetweenPointAndArc(
point: Point,
startPoint: Point,
arcCommand: SVGArcCommand
): Point {
export function getNearestPointBetweenPointAndArc(point: Point, startPoint: Point, arcCommand: SVGArcCommand): Point {
const { center, rx, ry } = getEllipseArcCenter(startPoint, arcCommand);

// 获取椭圆上的最近点
const nearestPoint = getNearestPointBetweenPointAndEllipse(
point,
center,
rx,
ry
);
const nearestPoint = getNearestPointBetweenPointAndEllipse(point, center, rx, ry);

// 判断最近点是否在弧段上
const startAngle = Math.atan2(startPoint[1] - center[1], startPoint[0] - center[0]);
Expand All @@ -563,14 +566,14 @@ export function getNearestPointBetweenPointAndArc(
function isAngleBetween(angle: number, start: number, end: number, clockwise: boolean): boolean {
// 标准化角度到 [0, 2π]
const normalize = (a: number) => ((a % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);

const a = normalize(angle);
const s = normalize(start);
const e = normalize(end);

if (clockwise) {
return s <= e ? (a >= s && a <= e) : (a >= s || a <= e);
return s <= e ? a >= s && a <= e : a >= s || a <= e;
} else {
return s >= e ? (a <= s && a >= e) : (a <= s || a >= e);
return s >= e ? a <= s && a >= e : a <= s || a >= e;
}
}
124 changes: 79 additions & 45 deletions packages/draw/src/engines/uml/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import {
Point,
PointOfRectangle,
RectangleClient,
SVGArcCommand,
W,
distanceBetweenPointAndPoint,
getEllipseTangentSlope,
getNearestPointBetweenPointAndDiscreteSegments,
getNearestPointBetweenPointAndEllipse,
getNearestPointBetweenPointAndSegment,
getNearestPointBetweenPointAndSegments,
getVectorFromPointAndSlope,
setStrokeLinecap
Expand All @@ -16,67 +20,97 @@ import { getPolygonEdgeByConnectionPoint } from '../../utils/polygon';
import { RectangleEngine } from '../basic-shapes/rectangle';
import { getUnitVectorByPointAndPoint, rotateVector } from '@plait/common';

interface ActorPathData {
headArcCommand: SVGArcCommand;
bodyLine: [Point, Point];
armsLine: [Point, Point];
leftLegLine: [Point, Point];
rightLegLine: [Point, Point];
}

function generateActorPath(rectangle: RectangleClient): ActorPathData {
const centerX = rectangle.x + rectangle.width / 2;
const headRadius = { width: rectangle.width / 3 / 2, height: rectangle.height / 4 / 2 };
const centerY = rectangle.y + rectangle.height / 4 / 2;

return {
headArcCommand: {
rx: headRadius.width,
ry: headRadius.height,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
endX: centerX,
endY: rectangle.y
},
bodyLine: [
[centerX, rectangle.y + rectangle.height / 4],
[centerX, rectangle.y + (rectangle.height / 4) * 3]
],
armsLine: [
[rectangle.x, rectangle.y + rectangle.height / 2],
[rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2]
],
leftLegLine: [
[centerX, rectangle.y + (rectangle.height / 4) * 3],
[rectangle.x + rectangle.width / 12, rectangle.y + rectangle.height]
],
rightLegLine: [
[centerX, rectangle.y + (rectangle.height / 4) * 3],
[rectangle.x + (rectangle.width / 12) * 11, rectangle.y + rectangle.height]
]
};
}

export const ActorEngine: ShapeEngine = {
draw(board: PlaitBoard, rectangle: RectangleClient, options: Options) {
const rs = PlaitBoard.getRoughSVG(board);
const shape = rs.path(
`M${rectangle.x + rectangle.width / 2} ${rectangle.y + rectangle.height / 4}
A${rectangle.width / 3 / 2} ${rectangle.height / 4 / 2}, 0, 0, 1, ${rectangle.x + rectangle.width / 2} ${rectangle.y}
A${rectangle.width / 3 / 2} ${rectangle.height / 4 / 2}, 0, 0, 1, ${rectangle.x + rectangle.width / 2} ${rectangle.y +
rectangle.height / 4}
V${rectangle.y + (rectangle.height / 4) * 3}
M${rectangle.x + rectangle.width / 2} ${rectangle.y + rectangle.height / 2} H${rectangle.x}
M${rectangle.x + rectangle.width / 2} ${rectangle.y + rectangle.height / 2} H${rectangle.x + rectangle.width}
M${rectangle.x + rectangle.width / 2} ${rectangle.y + (rectangle.height / 4) * 3}
L${rectangle.x + rectangle.width / 12} ${rectangle.y + rectangle.height}
M${rectangle.x + rectangle.width / 2} ${rectangle.y + (rectangle.height / 4) * 3}
L${rectangle.x + (rectangle.width / 12) * 11} ${rectangle.y + rectangle.height}
`,
{ ...options, fillStyle: 'solid' }
);
const { headArcCommand, bodyLine, armsLine, leftLegLine, rightLegLine } = generateActorPath(rectangle);

const pathData = [
// 头部(从中间开始画)
`M${bodyLine[0][0]} ${bodyLine[0][1]}`,
`A${headArcCommand.rx} ${headArcCommand.ry} ${headArcCommand.xAxisRotation} ${headArcCommand.largeArcFlag} ${headArcCommand.sweepFlag} ${headArcCommand.endX} ${headArcCommand.endY}`,
`A${headArcCommand.rx} ${headArcCommand.ry} ${headArcCommand.xAxisRotation} ${headArcCommand.largeArcFlag} ${headArcCommand.sweepFlag} ${bodyLine[0][0]} ${bodyLine[0][1]}`,
// 身体
`V${bodyLine[1][1]}`,
// 手臂
`M${armsLine[0][0]} ${armsLine[0][1]} H${armsLine[1][0]}`,
// 腿
`M${leftLegLine[0][0]} ${leftLegLine[0][1]} L${leftLegLine[1][0]} ${leftLegLine[1][1]}`,
`M${rightLegLine[0][0]} ${rightLegLine[0][1]} L${rightLegLine[1][0]} ${rightLegLine[1][1]}`
].join(' ');

const shape = rs.path(pathData, { ...options, fillStyle: 'solid' });
setStrokeLinecap(shape, 'round');
return shape;
},

getNearestPoint(rectangle: RectangleClient, point: Point) {
const { headArcCommand, bodyLine, armsLine, leftLegLine, rightLegLine } = generateActorPath(rectangle);

// 检查头部椭圆
const headCenter: Point = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 4 / 2];
const nearestPointForHead = getNearestPointBetweenPointAndEllipse(point, headCenter, headArcCommand.rx, headArcCommand.ry);
const distanceForHead = distanceBetweenPointAndPoint(...point, ...nearestPointForHead);

// 检查所有线段
const allSegments = [bodyLine, armsLine, leftLegLine, rightLegLine];
const nearestPointForLines = getNearestPointBetweenPointAndDiscreteSegments(point, allSegments);
const distanceForLines = distanceBetweenPointAndPoint(...point, ...nearestPointForLines);

return distanceForHead < distanceForLines ? nearestPointForHead : nearestPointForLines;
},
isInsidePoint(rectangle: RectangleClient, point: Point) {
const rangeRectangle = RectangleClient.getRectangleByPoints([point, point]);
return RectangleClient.isHit(rectangle, rangeRectangle);
},
getCornerPoints(rectangle: RectangleClient) {
return RectangleClient.getCornerPoints(rectangle);
},
getNearestPoint(rectangle: RectangleClient, point: Point) {
let nearestPoint = getNearestPointBetweenPointAndSegments(point, RectangleEngine.getCornerPoints(rectangle));

if (nearestPoint[1] >= rectangle.y && nearestPoint[1] <= rectangle.y + rectangle.height / 4) {
const centerPoint: Point = [rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 4 / 2];
nearestPoint = getNearestPointBetweenPointAndEllipse(point, centerPoint, rectangle.width / 3 / 2, rectangle.height / 4 / 2);
return nearestPoint;
}
if (nearestPoint[1] >= rectangle.y + rectangle.height / 4 && nearestPoint[1] < rectangle.y + (rectangle.height / 4) * 3) {
if (nearestPoint[1] === rectangle.x + rectangle.width / 2) {
nearestPoint = getNearestPointBetweenPointAndSegments(point, [
[rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 4],
[rectangle.x + rectangle.width / 2, rectangle.y + (rectangle.height / 4) * 3]
]);
} else {
nearestPoint = getNearestPointBetweenPointAndSegments(point, [
[rectangle.x, rectangle.y + rectangle.height / 2],
[rectangle.x + rectangle.width, rectangle.y + rectangle.height / 2]
]);
}
return nearestPoint;
}
nearestPoint = getNearestPointBetweenPointAndSegments(point, [
[rectangle.x + rectangle.width / 12, rectangle.y + rectangle.height],
[rectangle.x + rectangle.width / 2, rectangle.y + (rectangle.height / 4) * 3],
[rectangle.x + (rectangle.width / 12) * 11, rectangle.y + rectangle.height]
]);
return nearestPoint;
},
getConnectorPoints(rectangle: RectangleClient) {
return RectangleClient.getEdgeCenterPoints(rectangle);
},

getTangentVectorByConnectionPoint(rectangle: RectangleClient, pointOfRectangle: PointOfRectangle) {
const connectionPoint = RectangleClient.getConnectionPoint(rectangle, pointOfRectangle);
if (connectionPoint[1] >= rectangle.y && connectionPoint[1] <= rectangle.y + rectangle.height / 4) {
Expand Down
2 changes: 1 addition & 1 deletion packages/draw/src/plugins/with-arrow-line-text-move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const withArrowLineTextMove = (board: PlaitBoard) => {
if (element) {
const movingPoint = resizeState.endPoint;
const points = getArrowLinePoints(board, element);
const distance = distanceBetweenPointAndSegments(points, movingPoint);
const distance = distanceBetweenPointAndSegments(movingPoint, points);
if (distance <= movableBuffer) {
const point = getNearestPointBetweenPointAndSegments(movingPoint, points, false);
const position = getRatioByPoint(points, point);
Expand Down
2 changes: 1 addition & 1 deletion packages/draw/src/utils/hit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const isHitArrowLineText = (board: PlaitBoard, element: PlaitArrowLine, p
};

export const isHitPolyLine = (pathPoints: Point[], point: Point) => {
const distance = distanceBetweenPointAndSegments(pathPoints, point);
const distance = distanceBetweenPointAndSegments(point, pathPoints);
return distance <= HIT_DISTANCE_BUFFER;
};

Expand Down

0 comments on commit fb47704

Please sign in to comment.