packages/shared/src/coordinate.ts (494 lines of code) (raw):

import { isValidNumber } from './types' export interface IPoint { x: number y: number } export interface ISize { width: number height: number } export interface ILineSegment { start: IPoint end: IPoint } export interface IRectEdgeLines { v: ILineSegment[] h: ILineSegment[] } export function isRect(rect: any): rect is IRect { return rect?.x && rect?.y && rect?.width && rect?.height } export function isPoint(val: any): val is IPoint { return isValidNumber(val?.x) && isValidNumber(val?.y) } export function isLineSegment(val: any): val is ILineSegment { return isPoint(val?.start) && isPoint(val?.end) } export class Point implements IPoint { x: number y: number constructor(x: number, y: number) { this.x = x this.y = y } } export class Rect implements IRect { x = 0 y = 0 width = 0 height = 0 constructor(x: number, y: number, width: number, height: number) { this.x = x this.y = y this.width = width this.height = height } get left() { return this.x } get right() { return this.x + this.width } get top() { return this.y } get bottom() { return this.y + this.height } } export class LineSegment { start: IPoint end: IPoint constructor(start: IPoint, end: IPoint) { this.start = { ...start } this.end = { ...end } } } export interface IRect { x: number y: number width: number height: number } export enum RectQuadrant { Inner1 = 'I1', //内部第一象限 Inner2 = 'I2', //内部第二象限 Inner3 = 'I3', //内部第三象限 Inner4 = 'I4', //内部第四象限 Outer1 = 'O1', //内部第五象限 Outer2 = 'O2', //内部第六象限 Outer3 = 'O3', //内部第七象限 Outer4 = 'O4', //内部第八象限 } export interface IPointToRectRelative { quadrant: RectQuadrant distance: number } export function isPointInRect(point: IPoint, rect: IRect, sensitive = true) { const boundSensor = (value: number) => { if (!sensitive) return 0 const sensor = value * 0.1 if (sensor > 20) return 20 if (sensor < 10) return 10 return sensor } return ( point.x >= rect.x + boundSensor(rect.width) && point.x <= rect.x + rect.width - boundSensor(rect.width) && point.y >= rect.y + boundSensor(rect.height) && point.y <= rect.y + rect.height - boundSensor(rect.height) ) } export function isEqualRect(target: IRect, source: IRect) { return ( target?.x === source?.x && target.y === source.y && target.width === source.width && target.height === source.height ) } export function getRectPoints(source: IRect) { const p1 = new Point(source.x, source.y) const p2 = new Point(source.x + source.width, source.y) const p3 = new Point(source.x + source.width, source.y + source.height) const p4 = new Point(source.x, source.y + source.height) return [p1, p2, p3, p4] } export function isRectInRect(target: IRect, source: IRect) { const [p1, p2, p3, p4] = getRectPoints(target) return ( isPointInRect(p1, source, false) && isPointInRect(p2, source, false) && isPointInRect(p3, source, false) && isPointInRect(p4, source, false) ) } export function isCrossRectInRect(target: IRect, source: IRect) { const targetCenterPoint = new Point( target.x + target.width / 2, target.y + target.height / 2 ) const sourceCenterPoint = new Point( source.x + source.width / 2, source.y + source.height / 2 ) return ( Math.abs(targetCenterPoint.x - sourceCenterPoint.x) <= target.width / 2 + source.width / 2 && Math.abs(targetCenterPoint.y - sourceCenterPoint.y) <= target.height / 2 + source.height / 2 ) } /** * 计算点在矩形的哪个象限 * @param point * @param rect */ export function calcQuadrantOfPointToRect(point: IPoint, rect: IRect) { const isInner = isPointInRect(point, rect) if (point.x <= rect.x + rect.width / 2) { if (point.y <= rect.y + rect.height / 2) { if (isInner) { return RectQuadrant.Inner1 } else { return RectQuadrant.Outer1 } } else { if (isInner) { return RectQuadrant.Inner4 } else { return RectQuadrant.Outer4 } } } else { if (point.y <= rect.y + rect.height / 2) { if (isInner) { return RectQuadrant.Inner2 } else { return RectQuadrant.Outer2 } } else { if (isInner) { return RectQuadrant.Inner3 } else { return RectQuadrant.Outer3 } } } } export function calcDistanceOfPointToRect(point: IPoint, rect: IRect) { let minX = Math.min( Math.abs(point.x - rect.x), Math.abs(point.x - (rect.x + rect.width)) ) let minY = Math.min( Math.abs(point.y - rect.y), Math.abs(point.y - (rect.y + rect.height)) ) if (point.x >= rect.x && point.x <= rect.x + rect.width) { minX = 0 } if (point.y >= rect.y && point.y <= rect.y + rect.height) { minY = 0 } return Math.sqrt(minX ** 2 + minY ** 2) } export function calcDistancePointToEdge(point: IPoint, rect: IRect) { const distanceTop = Math.abs(point.y - rect.y) const distanceBottom = Math.abs(point.y - (rect.y + rect.height)) const distanceLeft = Math.abs(point.x - rect.x) const distanceRight = Math.abs(point.x - (rect.x + rect.width)) return Math.min(distanceTop, distanceBottom, distanceLeft, distanceRight) } export function isNearAfter(point: IPoint, rect: IRect, inline = false) { if (inline) { return ( Math.abs(point.x - rect.x) + Math.abs(point.y - rect.y) > Math.abs(point.x - (rect.x + rect.width)) + Math.abs(point.y - (rect.y + rect.height)) ) } return Math.abs(point.y - rect.y) > Math.abs(point.y - (rect.y + rect.height)) } /** * 计算点鱼矩形的相对位置信息 * @param point * @param rect */ export function calcRelativeOfPointToRect( point: IPoint, rect: IRect ): IPointToRectRelative { const distance = calcDistanceOfPointToRect(point, rect) const quadrant = calcQuadrantOfPointToRect(point, rect) return { quadrant, distance, } } export function calcBoundingRect(rects: IRect[]) { if (!rects?.length) return if (rects?.length === 1 && !rects[0]) return let minTop = Infinity let maxBottom = -Infinity let minLeft = Infinity let maxRight = -Infinity rects.forEach((item) => { const rect = new Rect(item.x, item.y, item.width, item.height) if (rect.top <= minTop) { minTop = rect.top } if (rect.bottom >= maxBottom) { maxBottom = rect.bottom } if (rect.left <= minLeft) { minLeft = rect.left } if (rect.right >= maxRight) { maxRight = rect.right } }) return new Rect(minLeft, minTop, maxRight - minLeft, maxBottom - minTop) } export function calcRectByStartEndPoint( startPoint: IPoint, endPoint: IPoint, scrollX = 0, scrollY = 0 ) { let drawStartX = 0, drawStartY = 0 if ( endPoint.x + scrollX >= startPoint.x && endPoint.y + scrollY >= startPoint.y ) { //4象限 drawStartX = startPoint.x drawStartY = startPoint.y return new Rect( drawStartX - scrollX, drawStartY - scrollY, Math.abs(endPoint.x - startPoint.x + scrollX), Math.abs(endPoint.y - startPoint.y + scrollY) ) } else if ( endPoint.x + scrollX < startPoint.x && endPoint.y + scrollY < startPoint.y ) { //1象限 drawStartX = endPoint.x drawStartY = endPoint.y return new Rect( drawStartX, drawStartY, Math.abs(endPoint.x - startPoint.x + scrollX), Math.abs(endPoint.y - startPoint.y + scrollY) ) } else if ( endPoint.x + scrollX < startPoint.x && endPoint.y + scrollY >= startPoint.y ) { //3象限 drawStartX = endPoint.x drawStartY = startPoint.y return new Rect( drawStartX - scrollX, drawStartY - scrollY, Math.abs(endPoint.x - startPoint.x + scrollX), Math.abs(endPoint.y - startPoint.y + scrollY) ) } else { //2象限 drawStartX = startPoint.x drawStartY = endPoint.y return new Rect( drawStartX, drawStartY, Math.abs(endPoint.x - startPoint.x + scrollX), Math.abs(endPoint.y - startPoint.y + scrollY) ) } } export function calcEdgeLinesOfRect(rect: IRect): IRectEdgeLines { return { v: [ new LineSegment( new Point(rect.x, rect.y), new Point(rect.x, rect.y + rect.height) ), new LineSegment( new Point(rect.x + rect.width / 2, rect.y), new Point(rect.x + rect.width / 2, rect.y + rect.height) ), new LineSegment( new Point(rect.x + rect.width, rect.y), new Point(rect.x + rect.width, rect.y + rect.height) ), ], h: [ new LineSegment( new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y) ), new LineSegment( new Point(rect.x, rect.y + rect.height / 2), new Point(rect.x + rect.width, rect.y + rect.height / 2) ), new LineSegment( new Point(rect.x, rect.y + rect.height), new Point(rect.x + rect.width, rect.y + rect.height) ), ], } } export function calcRectOfAxisLineSegment(line: ILineSegment) { if (!isLineSegment(line)) return const isXAxis = line.start.x === line.end.x return new Rect( line.start.x, line.start.y, isXAxis ? 0 : line.end.x - line.start.x, isXAxis ? line.end.y - line.start.y : 0 ) } export function calcSpaceBlockOfRect( target: IRect, source: IRect, type?: string ) { const targetRect = new Rect(target.x, target.y, target.width, target.height) const sourceRect = new Rect(source.x, source.y, source.width, source.height) if (sourceRect.bottom < targetRect.top && sourceRect.left > targetRect.right) return if (sourceRect.top > targetRect.bottom && sourceRect.left > targetRect.right) return if (sourceRect.bottom < targetRect.top && sourceRect.right < targetRect.left) return if (sourceRect.top > targetRect.bottom && sourceRect.right < targetRect.left) return if (sourceRect.bottom < targetRect.top) { const distance = targetRect.top - sourceRect.bottom const left = Math.min(sourceRect.left, targetRect.left) const right = Math.max(sourceRect.right, targetRect.right) if (type && type !== 'top') return return { type: 'top', distance, rect: new Rect(left, sourceRect.bottom, right - left, distance), } } else if (sourceRect.top > targetRect.bottom) { const distance = sourceRect.top - targetRect.bottom const left = Math.min(sourceRect.left, targetRect.left) const right = Math.max(sourceRect.right, targetRect.right) if (type && type !== 'bottom') return return { type: 'bottom', distance, rect: new Rect(left, targetRect.bottom, right - left, distance), } } else if (sourceRect.right < targetRect.left) { const distance = targetRect.left - sourceRect.right const top = Math.min(sourceRect.top, targetRect.top) const bottom = Math.max(sourceRect.bottom, targetRect.bottom) if (type && type !== 'left') return return { type: 'left', distance, rect: new Rect(sourceRect.right, top, distance, bottom - top), } } else if (sourceRect.left > targetRect.right) { const distance = sourceRect.left - targetRect.right const top = Math.min(sourceRect.top, targetRect.top) const bottom = Math.max(sourceRect.bottom, targetRect.bottom) if (type && type !== 'right') return return { type: 'right', distance, rect: new Rect(targetRect.right, top, distance, bottom - top), } } } export function calcExtendsLineSegmentOfRect( targetRect: Rect, referRect: Rect ) { if ( referRect.right < targetRect.right && targetRect.left <= referRect.right ) { //右侧 if (referRect.bottom < targetRect.top) { //上方 return { start: { x: referRect.right, y: referRect.bottom }, end: { x: targetRect.right, y: referRect.bottom }, } } else if (referRect.top > targetRect.bottom) { //下方 return { start: { x: referRect.right, y: referRect.top }, end: { x: targetRect.right, y: referRect.top }, } } } else if ( referRect.left > targetRect.left && targetRect.right >= referRect.left ) { //左侧 if (referRect.bottom < targetRect.top) { //上方 return { start: { x: targetRect.left, y: referRect.bottom }, end: { x: referRect.left, y: referRect.bottom }, } } else if (referRect.top > targetRect.bottom) { //下方 return { start: { x: targetRect.left, y: referRect.top }, end: { x: referRect.left, y: referRect.top }, } } } if (referRect.top < targetRect.top && targetRect.bottom >= referRect.top) { //refer在上方 if (referRect.right < targetRect.left) { //右侧 return { start: { x: referRect.right, y: referRect.bottom }, end: { x: referRect.right, y: targetRect.bottom }, } } else if (referRect.left > targetRect.right) { //左侧 return { start: { x: referRect.left, y: referRect.bottom }, end: { x: referRect.left, y: targetRect.bottom }, } } } else if ( referRect.bottom > targetRect.bottom && referRect.top <= targetRect.bottom ) { //refer下方 if (referRect.right < targetRect.left) { //右侧 return { start: { x: referRect.right, y: targetRect.top }, end: { x: referRect.right, y: referRect.top }, } } else if (referRect.left > targetRect.right) { //左侧 return { start: { x: referRect.left, y: targetRect.top }, end: { x: referRect.left, y: referRect.top }, } } } } export function calcOffsetOfSnapLineSegmentToEdge( line: ILineSegment, current: IRect ) { const edges = calcEdgeLinesOfRect(current) const isVerticalLine = line.start.x === line.end.x if (isVerticalLine) { return { x: calcMinDistanceValue(edges.x, line.start.x) - current.x, y: 0 } } function calcEdgeLinesOfRect(rect: IRect) { return { x: [rect.x, rect.x + rect.width / 2, rect.x + rect.width], y: [rect.y, rect.y + rect.height / 2, rect.y + rect.height], } } function calcMinDistanceValue(edges: number[], targetValue: number) { let minDistance = Infinity, minDistanceIndex = -1 for (let i = 0; i < edges.length; i++) { const distance = Math.abs(edges[i] - targetValue) if (minDistance > distance) { minDistance = distance minDistanceIndex = i } } return edges[minDistanceIndex] } return { x: 0, y: calcMinDistanceValue(edges.y, line.start.y) - current.y } } export function calcDistanceOfSnapLineToEdges( line: ILineSegment, edges: IRectEdgeLines ) { let distance = Infinity if (line?.start?.y === line?.end?.y) { edges.h.forEach((target) => { const _distance = Math.abs(target.start.y - line.start.y) if (_distance < distance) { distance = _distance } }) } else if (line?.start?.x === line?.end?.x) { edges.v.forEach((target) => { const _distance = Math.abs(target.start.x - line.start.x) if (_distance < distance) { distance = _distance } }) } else { throw new Error('can not calculate slash distance') } return distance } export function calcCombineSnapLineSegment( target: ILineSegment, source: ILineSegment ): ILineSegment { if (target.start.x === target.end.x) { return new LineSegment( new Point( target.start.x, target.start.y > source.start.y ? source.start.y : target.start.y ), new Point( target.start.x, target.end.y > source.end.y ? target.end.y : source.end.y ) ) } return new LineSegment( new Point( target.start.x > source.start.x ? source.start.x : target.start.x, target.start.y ), new Point( target.end.x > source.end.x ? target.end.x : source.end.x, target.end.y ) ) } export function calcClosestEdges( line: ILineSegment, edges: IRectEdgeLines ): [number, ILineSegment] { let result: ILineSegment let distance = Infinity if (line?.start?.y === line?.end?.y) { edges.h.forEach((target) => { const _distance = Math.abs(target.start.y - line.start.y) if (_distance < distance) { distance = _distance result = target } }) } else if (line?.start?.x === line?.end?.x) { edges.v.forEach((target) => { const _distance = Math.abs(target.start.x - line.start.x) if (_distance < distance) { distance = _distance result = target } }) } else { throw new Error('can not calculate slash distance') } return [distance, result] }