diff --git a/packages/core/src/utils/math.ts b/packages/core/src/utils/math.ts index 709781765..4a39c8408 100644 --- a/packages/core/src/utils/math.ts +++ b/packages/core/src/utils/math.ts @@ -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) { @@ -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, @@ -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; @@ -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]); @@ -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; } } diff --git a/packages/draw/src/engines/uml/actor.ts b/packages/draw/src/engines/uml/actor.ts index 91c42817d..fc25f0956 100644 --- a/packages/draw/src/engines/uml/actor.ts +++ b/packages/draw/src/engines/uml/actor.ts @@ -3,9 +3,13 @@ import { Point, PointOfRectangle, RectangleClient, + SVGArcCommand, W, + distanceBetweenPointAndPoint, getEllipseTangentSlope, + getNearestPointBetweenPointAndDiscreteSegments, getNearestPointBetweenPointAndEllipse, + getNearestPointBetweenPointAndSegment, getNearestPointBetweenPointAndSegments, getVectorFromPointAndSlope, setStrokeLinecap @@ -16,27 +20,87 @@ 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); @@ -44,39 +108,9 @@ export const ActorEngine: ShapeEngine = { 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) { diff --git a/packages/draw/src/plugins/with-arrow-line-text-move.ts b/packages/draw/src/plugins/with-arrow-line-text-move.ts index 535bcbe09..a4acd5eb3 100644 --- a/packages/draw/src/plugins/with-arrow-line-text-move.ts +++ b/packages/draw/src/plugins/with-arrow-line-text-move.ts @@ -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); diff --git a/packages/draw/src/utils/hit.ts b/packages/draw/src/utils/hit.ts index de8c88cbe..1b7c9d632 100644 --- a/packages/draw/src/utils/hit.ts +++ b/packages/draw/src/utils/hit.ts @@ -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; };