import { Adsorber } from './adsorber'; import { PositionSchema } from '../../common/schema'; import { Rectangle } from '@gedit/math'; const isHorizontal = (direction: Adsorber.Direction) => Adsorber.Direction.HORIZONTAL === direction; const reverseDirection = (direction: Adsorber.Direction) => isHorizontal(direction) ? Adsorber.Direction.VERTICAL : Adsorber.Direction.HORIZONTAL; function equalNearly(num1: number, num2: number, delta = 1): boolean { return Math.abs(num1 - num2) <= delta; } export enum AlignType { LINE, // 线 LINE_BETWEEN, // 间距线 BLOCK_BETWEEN, // 块间距 } enum AlignPlace { BEGIN, MIDDLE, END, } export interface AlignInfo { type: AlignType; direction: Adsorber.Direction; startPos: PositionSchema; endPos: PositionSchema; } interface AlignPos { startPos: PositionSchema; endPos: PositionSchema; } function getAlignPos( target: Rectangle, place: AlignPlace, direction: Adsorber.Direction ): AlignPos { switch (place) { case AlignPlace.BEGIN: if (isHorizontal(direction)) { return { startPos: target.leftTop, endPos: target.rightTop, }; } return { startPos: target.leftTop, endPos: target.leftBottom, }; case AlignPlace.MIDDLE: if (isHorizontal(direction)) { return { startPos: target.leftCenter, endPos: target.rightCenter, }; } return { startPos: target.topCenter, endPos: target.bottomCenter, }; case AlignPlace.END: if (isHorizontal(direction)) { return { startPos: target.leftBottom, endPos: target.rightBottom, }; } return { startPos: target.rightTop, endPos: target.rightBottom, }; } } /** * 对齐线 */ function getLineInfo( target: Rectangle, targetPlace: AlignPlace, ref: Rectangle, refPlace: AlignPlace, direction: Adsorber.Direction ): AlignInfo { const targetPos = getAlignPos(target, targetPlace, direction); const refPos = getAlignPos(ref, refPlace, direction); if (isHorizontal(direction)) { return { type: AlignType.LINE, direction, startPos: targetPos.startPos.x < refPos.startPos.x ? targetPos.startPos : refPos.startPos, endPos: targetPos.endPos.x > refPos.endPos.x ? targetPos.endPos : refPos.endPos, }; } return { type: AlignType.LINE, direction, startPos: targetPos.startPos.y < refPos.startPos.y ? targetPos.startPos : refPos.startPos, endPos: targetPos.endPos.y > refPos.endPos.y ? targetPos.endPos : refPos.endPos, }; } /** * 间距线 */ function getLineBetweenInfo( target: Rectangle, targetPlace: AlignPlace, ref: Rectangle, refPlace: AlignPlace, direction: Adsorber.Direction ): AlignInfo | undefined { // 判断距离不交叉 if ( !Rectangle.intersects( target, ref, isHorizontal(direction) ? 'horizantal' : 'vertical' ) ) { const targetPos = getAlignPos(target, targetPlace, direction); const refPos = getAlignPos(ref, refPlace, direction); if (isHorizontal(direction)) { const result = { type: AlignType.LINE_BETWEEN, direction, startPos: { x: refPos.startPos.x < targetPos.startPos.x ? refPos.endPos.x : targetPos.endPos.x, y: targetPos.startPos.y, }, endPos: { x: refPos.startPos.x < targetPos.startPos.x ? targetPos.startPos.x : refPos.startPos.x, y: targetPos.endPos.y, }, }; // 距离为0不显示 if (equalNearly(result.startPos.x, result.endPos.x)) return undefined; return result; } const resultY = { type: AlignType.LINE_BETWEEN, direction, startPos: { x: targetPos.startPos.x, y: refPos.startPos.y < targetPos.startPos.y ? refPos.endPos.y : targetPos.endPos.y, }, endPos: { x: targetPos.endPos.x, y: refPos.startPos.y < targetPos.startPos.y ? targetPos.startPos.y : refPos.startPos.y, }, }; // 距离为0不显示 if (equalNearly(resultY.startPos.y, resultY.endPos.y)) return undefined; return resultY; } } /** * 添加或替换间距线,同一位置取最短 * @param list * @param target */ function pushLineBetweenInfo(list: AlignInfo[], target: AlignInfo): void { let replaced = false; list.forEach((l, index) => { if (l.type === AlignType.LINE_BETWEEN && l.direction === target.direction) { if (isHorizontal(target.direction)) { // 同一行 if (equalNearly(l.startPos.y, target.startPos.y)) { // 取最短 if (l.endPos.x - l.startPos.x > target.endPos.x - target.startPos.x) { list[index] = target; } replaced = true; } } else { // 同一列 if (equalNearly(l.startPos.x, target.startPos.x)) { // 取最短 if (l.endPos.y - l.startPos.y > target.endPos.y - target.startPos.y) { list[index] = target; } replaced = true; } } } }); if (!replaced) list.push(target); } function calculateAlign( target: Rectangle, refs: Rectangle[], // 参考对象 lines: Adsorber.Line[], // 参考线 direction: Adsorber.Direction ): AlignInfo[] { const targetValues = Adsorber.getRefValues(target, direction); const refValuesList = refs.map(ref => Adsorber.getRefValues(ref, direction) ); const result: AlignInfo[] = []; const directionReverse = reverseDirection(direction); for (let i = 0, len = refValuesList.length; i < len; i++) { const ref = refValuesList[i]; const [begin, middle, end] = ref; let hit: boolean = false; let hitRefPlace: AlignPlace | undefined; let hitTargetPlace: AlignPlace | undefined; // middle 的优先级最高 targetValues.forEach((v, index) => { if (equalNearly(v, begin)) { result.push( getLineInfo( target, index, refs[i], AlignPlace.BEGIN, directionReverse ) ); hitRefPlace = AlignPlace.BEGIN; if (hitTargetPlace !== AlignPlace.MIDDLE) hitTargetPlace = index; hit = true; } else if (equalNearly(v, end)) { result.push( getLineInfo(target, index, refs[i], AlignPlace.END, directionReverse) ); hitRefPlace = AlignPlace.END; if (hitTargetPlace !== AlignPlace.MIDDLE) hitTargetPlace = index; hit = true; } else if (equalNearly(v, middle)) { result.push( getLineInfo( target, index, refs[i], AlignPlace.MIDDLE, directionReverse ) ); hitRefPlace = AlignPlace.MIDDLE; if (hitTargetPlace !== AlignPlace.MIDDLE) hitTargetPlace = index; hit = true; } }); if (hit) { const info = getLineBetweenInfo( target, hitTargetPlace!, refs[i], hitRefPlace!, directionReverse ); if (info) { pushLineBetweenInfo(result, info); } } } return result; } interface AlignRect { top: number; right: number; bottom: number; left: number; } function getLineData(target: Rectangle, ref: Rectangle): AlignRect { let top = target.y - ref.y - ref.height; top = target.y > ref.y && top < 0 ? ref.y - target.y : top; let bottom = ref.y - target.y - target.height; bottom = bottom < 0 && ref.y + ref.height > target.y + target.height ? target.y + target.height - ref.y - ref.height : bottom; let right = ref.x - target.x - target.width; right = right < 0 && ref.x + ref.width > target.x + target.width ? target.x + target.width - ref.x - ref.width : right; let left = target.x - ref.x - ref.width; left = left < 0 && target.x > ref.x ? ref.x - target.x : left; return { top, right, bottom, left, }; } export namespace Align { export function calculate( target: Rectangle, refs: Rectangle[], // 参考对象 lines: Adsorber.Line[] // 参考线 ): AlignInfo[] { return calculateAlign( target, refs, lines, Adsorber.Direction.HORIZONTAL ).concat(calculateAlign(target, refs, lines, Adsorber.Direction.VERTICAL)); } /** * @param target 选中元素 * @param ref 参考元素 * @returns AlignInfo[]; * 计算所有对齐线 * 1. target 在 ref 内部,计算 target 四边到 ref 四边的距离, 有四条线; * 2. target 在 ref 外部: * 1) 在上边, target 下边到 ref 上边的距离,如上下重叠,计算 target 下边到 ref 下边的距离; * 2) 在右边, target 左边到 ref 右边的距离,如左右重叠,计算 target 右边到 ref 右边的距离; * 3) 在下边, target 上边到 ref 下边的距离,如上下重叠,计算 target 上边到 ref 上边的距离; * 4) 在左边, target 右边到 ref 左边的距离,如左右重叠,计算 target 左边到 ref 左边的距离; */ export function calculateAll(target: Rectangle, ref: Rectangle): AlignInfo[] { const result: AlignInfo[] = []; const targetHorizontalPoint = Adsorber.getRefValues( target, Adsorber.Direction.HORIZONTAL ); const targetVerticalPoint = Adsorber.getRefValues( target, Adsorber.Direction.VERTICAL ); const refHorizontalPoint = Adsorber.getRefValues( ref, Adsorber.Direction.HORIZONTAL ); const refVerticalPoint = Adsorber.getRefValues( ref, Adsorber.Direction.VERTICAL ); // 计算 target 四边到 ref 四边的距离 const rect: AlignRect = getLineData(target, ref); // in top let inner = rect.top < 0 && rect.bottom < 0; let b: number; // 参考物在内部计算; let refInner = ref.y > target.y; b = inner ? ref.y : ref.y + ref.height; if ((inner || rect.top > 0) && !equalNearly(b, target.y)) { result.push({ type: AlignType.LINE_BETWEEN, direction: Adsorber.Direction.VERTICAL, startPos: { x: refInner ? refHorizontalPoint[1] : targetHorizontalPoint[1], // 竖的中间点开始 y: refInner ? target.y : b, }, endPos: { x: refInner ? refHorizontalPoint[1] : targetHorizontalPoint[1], y: refInner ? b : target.y, }, }); } // in bottom b = inner ? ref.y + ref.height : ref.y; refInner = ref.y + ref.height < target.y + target.height; if ( (inner || rect.bottom > 0) && !equalNearly(b, target.y + target.height) ) { result.push({ type: AlignType.LINE_BETWEEN, direction: Adsorber.Direction.VERTICAL, startPos: { x: refInner ? refHorizontalPoint[1] : targetHorizontalPoint[1], y: refInner ? b : target.y + target.height, }, endPos: { x: refInner ? refHorizontalPoint[1] : targetHorizontalPoint[1], y: refInner ? target.y + target.height : b, }, }); } // in right inner = rect.right < 0 && rect.left < 0; refInner = ref.x + ref.width < target.x + target.width; b = inner ? ref.x + ref.width : ref.x; if ((inner || rect.right > 0) && !equalNearly(b, target.x + target.width)) { result.push({ type: AlignType.LINE_BETWEEN, direction: Adsorber.Direction.HORIZONTAL, startPos: { x: refInner ? b : target.x + target.width, y: refInner ? refVerticalPoint[1] : targetVerticalPoint[1], }, endPos: { x: refInner ? target.x + target.width : b, y: refInner ? refVerticalPoint[1] : targetVerticalPoint[1], }, }); } // in left b = inner ? ref.x : ref.x + ref.width; refInner = ref.x > target.x; if ((inner || rect.left > 0) && !equalNearly(b, target.x)) { result.push({ type: AlignType.LINE_BETWEEN, direction: Adsorber.Direction.HORIZONTAL, startPos: { x: refInner ? target.x : b, y: refInner ? refVerticalPoint[1] : targetVerticalPoint[1], }, endPos: { x: refInner ? b : target.x, y: refInner ? refVerticalPoint[1] : targetVerticalPoint[1], }, }); } return result; } }