import { PositionSchema, TransformData } from '../../common/schema'; import { Rectangle } from '@gedit/math'; import type { EntityManager, Entity } from '../../common'; import type { PlaygroundConfigEntity } from '../layer/config'; import { Adsorbable } from '../able/adsorbable'; // 吸附的最小距离 export const ADSORB_DELTA_DEFAULT = 4; // 吸附的位置 enum AdsorbNearestPlace { BEGIN, MIDDLE, END } /** * 吸附最接近的节点 * @param target * @param refs * @param lines * @param adSorbDelta * @param type */ function getAdsorbNearestDelta( target: Rectangle, refs: Rectangle[], lines: Adsorber.Line[], // 参考线 adSorbDelta: number, type: Adsorber.Direction): number | undefined { const targetValues = Adsorber.getRefValues(target, type); const refValuesList = refs.map(ref => Adsorber.getRefValues(ref, type)); let nearestDist: number = adSorbDelta; let nearestValue: number | undefined; let nearestPlace: AdsorbNearestPlace | undefined; for (let i = 0, len = refValuesList.length; i < len; i++) { const ref = refValuesList[i]; const [begin, middle, end] = ref; targetValues.forEach((v, index) => { const distBegin = Math.abs(v - begin); const distMiddle = Math.abs(v - middle); const distEnd = Math.abs(v - end); // 和开始点接近 if (distBegin < nearestDist) { nearestDist = distBegin; nearestValue = begin; nearestPlace = index; } // 和中间点接近 if (distMiddle < nearestDist) { nearestDist = distMiddle; nearestValue = middle; nearestPlace = index; } // 和结束点接近 if (distEnd < nearestDist) { nearestDist = distEnd; nearestValue = end; nearestPlace = index; } }); } // 接近参考线 for (let i = 0, len = lines.length; i < len; i++) { const line = lines[i]; if (line.type === type) { const lineValue = line.pos; targetValues.forEach((v, index) => { const dist = Math.abs(lineValue - v); if (dist < nearestDist) { nearestDist = dist; nearestValue = lineValue; nearestPlace = index; } }); } } if (nearestValue !== undefined) { const size = type === Adsorber.Direction.HORIZONTAL ? target.width : target.height; const pos = type === Adsorber.Direction.HORIZONTAL ? target.x : target.y; switch (nearestPlace) { case AdsorbNearestPlace.BEGIN: return nearestValue - pos; case AdsorbNearestPlace.MIDDLE: return nearestValue - size / 2 - pos; case AdsorbNearestPlace.END: return nearestValue - size - pos; } } } export class Adsorber { protected opts: Adsorber.Option; private _adsorbedX?: number; private _adsorbedY?: number; private _adsorbedDeltaX?: number; private _adsorbedDeltaY?: number; constructor( opts: Partial = {} ) { this.opts = Object.assign({}, Adsorber.defaultOption, opts); } /** * 计算吸附偏移量 * @param target - 目标选择框 * @param refs - 参考节点 * @param lines - 参考线 * @return 吸附后的偏移量, 如果为undefined,则表示没有被吸住,不做偏移 */ calculateDelta( target: Rectangle, refs: Rectangle[], lines: Adsorber.Line[], ): Partial { let x: number | undefined; let y: number | undefined; // 判断水平逃逸 if (this._adsorbedX !== undefined) { x = this.calculateEscape(target.x, Adsorber.Direction.HORIZONTAL); } else { // 水平吸附 x = getAdsorbNearestDelta(target, refs, lines, this.opts.adSorbDelta, Adsorber.Direction.HORIZONTAL); if (x !== undefined) { this._adsorbedX = target.x; this._adsorbedDeltaX = x; } } // 判断垂直逃逸 if (this._adsorbedY !== undefined) { y = this.calculateEscape(target.y, Adsorber.Direction.VERTICAL); } else { // 垂直吸附 y = getAdsorbNearestDelta(target, refs, lines, this.opts.adSorbDelta, Adsorber.Direction.VERTICAL); if (y !== undefined) { this._adsorbedY = target.y; this._adsorbedDeltaY = y; } } return { x, y, }; } /** * 计算吸附 * @param target - 目标选择框 * @param refs - 参考节点 * @param lines - 参考线 * @return 吸附后的位置 */ calculate( target: Rectangle, refs: Rectangle[], lines: Adsorber.Line[], ): PositionSchema { const delta = this.calculateDelta(target, refs, lines); const pos = { x: target.x, y: target.y }; // 吸附住了 if (delta.x !== undefined) { pos.x = this._adsorbedX! + this._adsorbedDeltaX!; } if (delta.y !== undefined) { pos.y = this._adsorbedY! + this._adsorbedDeltaY!; } return pos; } /** * 计算吸附后逃逸的偏移量 * @param newPos - 当前的位置 * @param type */ protected calculateEscape(newPos: number, type: Adsorber.Direction): number | undefined { const adsorbedPos = this.getAbsorbedPos(type); if (adsorbedPos !== undefined) { const dist = Math.abs(newPos - adsorbedPos); // 超出逃逸范围 if (dist > this.opts.adSorbDelta) { this.clearAbsorbedPos(type); return undefined; } } return 0; } /** * 吸附时候位置 */ protected getAbsorbedPos(type: Adsorber.Direction): number | undefined { return type === Adsorber.Direction.HORIZONTAL ? this._adsorbedX : this._adsorbedY; } /** * 清空吸附位置 * @param type */ clearAbsorbedPos(type?: Adsorber.Direction): void { if (type === Adsorber.Direction.HORIZONTAL) { this._adsorbedX = undefined; this._adsorbedDeltaX = undefined; } else if (type === Adsorber.Direction.VERTICAL ) { this._adsorbedY = undefined; this._adsorbedDeltaY = undefined; } else { this._adsorbedX = undefined; this._adsorbedY = undefined; this._adsorbedDeltaX = undefined; this._adsorbedDeltaY = undefined; } } } /** * 吸附计算 */ export namespace Adsorber { /** * 吸附配置数据 */ export interface Option { adSorbDelta: number, // 吸附调节值 } /** * 吸附参考线类型 */ export interface Line { type: Adsorber.Direction pos: number, } /** * 吸附类型 */ export enum Direction { HORIZONTAL, // 水平 VERTICAL, // 垂直 } export const defaultOption: Option = { adSorbDelta: ADSORB_DELTA_DEFAULT, }; export function isHorizantal(direction: Adsorber.Direction): boolean { return direction === Adsorber.Direction.HORIZONTAL; } export function getRefValues(target: Rectangle, direction: Adsorber.Direction): number[] { if (direction === Adsorber.Direction.HORIZONTAL) { return [ target.x, // 开始 target.x + target.width / 2, // 中间 target.x + target.width, // 尾部 ]; } return [ target.y, // 开始 target.y + target.height / 2, // 中间 target.y + target.height // 尾部 ]; } export function getRefsFromEntities(entityManager: EntityManager, ignoreEntities: Entity[], configEntity?: PlaygroundConfigEntity): Rectangle[] { const adsorbRefs = entityManager.getEntitiesByAble(Adsorbable) // 过滤掉正在拖动的对象 .filter(entity => !ignoreEntities.includes(entity) && !TransformData.isParentOrChildrenTransform(ignoreEntities, entity)) .map(node => node.getData(TransformData)!.bounds); if (configEntity) { // 画布的外围矩形 const pageBounds = configEntity.getPageBounds(); if (pageBounds) { adsorbRefs.push(pageBounds); } } return adsorbRefs; } }