import MoveableManager from "../MoveableManager"; import { Renderer, SnappableProps, SnappableState, Guideline, SnapInfo, BoundInfo, ScalableProps } from "../types"; import { prefix, caculatePoses, getRect, getAbsolutePosesByState, getAbsolutePoses } from "../utils"; import { directionCondition } from "../groupUtils"; import { isUndefined, IObject } from "@daybrush/utils"; import { getPosByReverseDirection, getPosesByDirection, getDragDist, scaleMatrix, getPosByDirection, } from "../DraggerUtils"; import { minus } from "@moveable/matrix"; function snapStart(moveable: MoveableManager) { const state = moveable.state; if (state.guidelines && state.guidelines.length) { return; } const { horizontalGuidelines = [], verticalGuidelines = [], elementGuidelines = [], bounds, snapCenter, } = moveable.props; if (!bounds && !horizontalGuidelines.length && !verticalGuidelines.length && !elementGuidelines.length) { return; } const { containerRect: { width: containerWidth, height: containerHeight, top: containerTop, left: containerLeft, }, clientRect: { top: clientTop, left: clientLeft, }, left: targetLeft, top: targetTop, } = state; const distLeft = targetLeft - (clientLeft - containerLeft); const distTop = targetTop - (clientTop - containerTop); const guidelines: Guideline[] = []; horizontalGuidelines!.forEach(pos => { guidelines.push({ type: "horizontal", pos: [0, pos], size: containerWidth }); }); verticalGuidelines!.forEach(pos => { guidelines.push({ type: "vertical", pos: [pos, 0], size: containerHeight }); }); elementGuidelines!.forEach(el => { const rect = el.getBoundingClientRect(); const { top, left, width, height } = rect; const elementTop = top - containerTop; const elementBottom = elementTop + height; const elementLeft = left - containerLeft; const elementRight = elementLeft + width; guidelines.push({ type: "vertical", element: el, pos: [elementLeft + distLeft, elementTop], size: height }); guidelines.push({ type: "vertical", element: el, pos: [elementRight + distLeft, elementTop], size: height }); guidelines.push({ type: "horizontal", element: el, pos: [elementLeft, elementTop + distTop], size: width }); guidelines.push({ type: "horizontal", element: el, pos: [elementLeft, elementBottom + distTop], size: width }); if (snapCenter) { guidelines.push({ type: "vertical", element: el, pos: [(elementLeft + elementRight) / 2 + distLeft, elementTop], size: height, center: true, }); guidelines.push({ type: "horizontal", element: el, pos: [elementLeft, (elementTop + elementBottom) / 2 + distTop], size: width, center: true, }); } }); state.guidelines = guidelines; state.enableSnap = true; } function checkBounds( moveable: MoveableManager, verticalPoses: number[], horizontalPoses: number[], snapThreshold?: number, ) { return { vertical: checkBound(moveable, verticalPoses, true, snapThreshold), horizontal: checkBound(moveable, horizontalPoses, false, snapThreshold), }; } function checkBound( moveable: MoveableManager, poses: number[], isVertical: boolean, snapThreshold: number = 0, ): BoundInfo { const bounds = moveable.props.bounds; if (bounds) { const startPos = bounds[isVertical ? "left" : "top"]; const endPos = bounds[isVertical ? "right" : "bottom"]; const minPos = Math.min(...poses); const maxPos = Math.max(...poses); if (!isUndefined(startPos) && startPos + snapThreshold > minPos) { return { isBound: true, offset: minPos - startPos, pos: startPos, }; } if (!isUndefined(endPos) && endPos - snapThreshold < maxPos) { return { isBound: true, offset: maxPos - endPos, pos: endPos, }; } } return { isBound: false, offset: 0, pos: 0, }; } function checkSnap( guidelines: Guideline[], targetType: "horizontal" | "vertical", targetPoses: number[], isSnapCenter: boolean | undefined, snapThreshold: number, ): SnapInfo { if (!guidelines) { return { isSnap: false, dist: -1, offset: 0, guidelines: [], snapPoses: [], }; } let snapGuidelines: Guideline[] = []; let snapDist = Infinity; let snapOffset = 0; const isVertical = targetType === "vertical"; const posType = isVertical ? 0 : 1; const snapPoses = targetPoses.filter(targetPos => { return guidelines.filter(guideline => { const { type, pos, center } = guideline; if ((!isSnapCenter && center) || type !== targetType) { return false; } const offset = targetPos - pos[posType]; const dist = Math.abs(offset); if (dist > snapThreshold) { return false; } if (snapDist > dist) { snapDist = dist; snapGuidelines = []; } if (snapDist === dist) { snapOffset = offset; snapGuidelines.push(guideline); } return true; }).length; }); return { isSnap: !!snapGuidelines.length, dist: isFinite(snapDist) ? snapDist : -1, offset: snapOffset, guidelines: snapGuidelines, snapPoses, }; } export function hasGuidelines( moveable: MoveableManager, ableName: string, ): moveable is MoveableManager { const { props: { snappable, bounds, }, state: { guidelines, enableSnap, }, } = moveable; if ( !snappable || !enableSnap || (ableName && snappable !== true && snappable.indexOf(ableName)) || (!bounds && (!guidelines || !guidelines.length)) ) { return false; } return true; } export function checkSnapPoses( moveable: MoveableManager, posesX: number[], posesY: number[], isSnapCenter?: boolean, customSnapThreshold?: number, ) { const guidelines = moveable.state.guidelines; const snapThreshold = !isUndefined(customSnapThreshold) ? customSnapThreshold : !isUndefined(moveable.props.snapThreshold) ? moveable.props.snapThreshold : 5; return { vertical: checkSnap(guidelines, "vertical", posesX, isSnapCenter, snapThreshold), horizontal: checkSnap(guidelines, "horizontal", posesY, isSnapCenter, snapThreshold), }; } export function checkSnaps( moveable: MoveableManager, rect: { left?: number, top?: number, bottom?: number, right?: number, center?: number, middle?: number, }, isCenter: boolean, customSnapThreshold?: number, ) { const snapCenter = moveable.props.snapCenter; const isSnapCenter = snapCenter! && isCenter; let verticalNames: Array<"left" | "center" | "right"> = ["left", "right"]; let horizontalNames: Array<"top" | "middle" | "bottom"> = ["top", "bottom"]; if (isSnapCenter) { verticalNames.push("center"); horizontalNames.push("middle"); } verticalNames = verticalNames.filter(name => name in rect); horizontalNames = horizontalNames.filter(name => name in rect); return checkSnapPoses( moveable, verticalNames.map(name => rect[name]!), horizontalNames.map(name => rect[name]!), isSnapCenter, customSnapThreshold, ); } export function getSize(x: number, y: number) { return Math.sqrt(x * x + y * y); } function checkBoundOneWayDist( moveable: MoveableManager, pos: number[], ) { const { horizontal: { isBound: isHorizontalBound, offset: horizontalBoundOffset, }, vertical: { isBound: isVerticalBound, offset: verticalBoundOffset, }, } = checkBounds( moveable, [pos[0]], [pos[1]], ); if (isHorizontalBound || isVerticalBound) { let isVertical!: boolean; if (isHorizontalBound && isVerticalBound) { isVertical = Math.abs(horizontalBoundOffset) < Math.abs(verticalBoundOffset); } else { isVertical = isVerticalBound; } const offset = isVertical ? verticalBoundOffset : horizontalBoundOffset; return { isVertical, offset, dist: Math.abs(offset), }; } return; } function solveNextDist( pos1: number[], pos2: number[], offset: number, isVertical: boolean, isDirectionVertical: boolean, datas: IObject, ) { const sizeOffset = solveEquation( pos1, pos2, -offset, isVertical, ); if (!sizeOffset) { return NaN; } const [widthDist, heightDist] = getDragDist({ datas, distX: sizeOffset[0], distY: sizeOffset[1], }); return isDirectionVertical ? heightDist : widthDist; } function getFixedPoses( matrix: number[], width: number, height: number, fixedPos: number[], direction: number[], is3d: boolean, ) { const nextPoses = caculatePoses(matrix, width, height, is3d ? 4 : 3); const nextPos = getPosByReverseDirection(nextPoses, direction); return getAbsolutePoses(nextPoses, minus(fixedPos, nextPos)); } function checkBoundOneWayPos( moveable: MoveableManager, pos: number[], reversePos: number[], isDirectionVertical: boolean, datas: any, ) { const { horizontal: { isSnap: isHorizontalSnap, offset: horizontalOffset, dist: horizontalDist, }, vertical: { isSnap: isVerticalSnap, offset: verticalOffset, dist: verticalDist, }, } = checkSnapPoses( moveable, [pos[0]], [pos[1]], ); const fixedHorizontal = reversePos[1] === pos[1]; const fixedVertical = reversePos[0] === pos[0]; let isVertical!: boolean; if (!isHorizontalSnap && !isVerticalSnap) { // no snap return NaN; } else if (isHorizontalSnap && isVerticalSnap) { if (horizontalDist === 0 && fixedHorizontal) { isVertical = true; } else if (verticalOffset === 0 && fixedVertical) { isVertical = false; } else { isVertical = horizontalDist > verticalDist; } } else { isVertical = isVerticalSnap; } return solveNextDist( reversePos, pos, (isVertical ? verticalOffset : horizontalOffset), isVertical, isDirectionVertical, datas, ); } export function checkOneWayPos( moveable: MoveableManager, poses: number[][], reversePoses: number[][], isDirectionVertical: boolean, datas: any, ) { let posOffset = 0; let boundInfo!: { isVertical: boolean, offset: number, dist: number, } | undefined; let boundIndex = -1; const boundInfos = poses.map(pos => checkBoundOneWayDist(moveable, pos)); boundInfos.forEach((info, i) => { if (!info) { return; } if (!boundInfo || boundInfo.dist < info.dist) { boundInfo = info; boundIndex = i; } }); if (boundInfo) { const nextDist = solveNextDist( reversePoses[boundIndex], poses[boundIndex], boundInfo.offset, boundInfo.isVertical, isDirectionVertical, datas, ); if (!isNaN(nextDist)) { posOffset = nextDist; } } else { poses.some((pos, i) => { const nextDist = checkBoundOneWayPos(moveable, pos, reversePoses[i], isDirectionVertical, datas); if (isNaN(nextDist)) { return false; } posOffset = nextDist; return true; }); } return posOffset; } export function checkOneWayDist( moveable: MoveableManager, poses: number[][], direction: number[], datas: any, ) { const directionPoses = getPosesByDirection(poses, direction); const reversePoses = poses.slice().reverse(); const directionIndex = direction[0] !== 0 ? 0 : 1; const isDirectionVertical = directionIndex > 0; const reverseDirectionPoses = getPosesByDirection(reversePoses, direction); directionPoses.push([ (directionPoses[0][0] + directionPoses[1][0]) / 2, (directionPoses[0][1] + directionPoses[1][1]) / 2, ]); reverseDirectionPoses.reverse(); reverseDirectionPoses.push([ (reverseDirectionPoses[0][0] + reverseDirectionPoses[1][0]) / 2, (reverseDirectionPoses[0][1] + reverseDirectionPoses[1][1]) / 2, ]); const posOffset = checkOneWayPos(moveable, directionPoses, reverseDirectionPoses, isDirectionVertical, datas); const offset = [0, 0]; offset[directionIndex] = direction[directionIndex] * posOffset; return offset; } export function checkTwoWayDist( moveable: MoveableManager, poses: number[][], direction: number[], datas: any, matrix: number[], width: number, height: number, fixedPos: number[], is3d: boolean, ) { const directionPoses = getPosesByDirection(poses, direction); const verticalDirection = [direction[0], direction[1] * -1]; const horizontalDirection = [direction[0] * -1, direction[1]]; const verticalPos = getPosByDirection(poses, verticalDirection); const horizontalPos = getPosByDirection(poses, horizontalDirection); const { horizontal: { isBound: isHorizontalBound, offset: horizontalBoundOffset, }, vertical: { isBound: isVerticalBound, offset: verticalBoundOffset, }, } = checkBounds( moveable, [directionPoses[0][0]], [directionPoses[0][1]], ); // share drag event let widthDist = 0; let heightDist = 0; const verticalBoundInfo = checkBoundOneWayDist(moveable, verticalPos); const horizontalBoundInfo = checkBoundOneWayDist(moveable, horizontalPos); const isVeritcalDirectionBound = verticalBoundInfo && verticalBoundInfo.dist > Math.abs(verticalBoundOffset); const isHorizontalDirectionBound = horizontalBoundInfo && horizontalBoundInfo.dist > Math.abs(horizontalBoundOffset); if (!isVeritcalDirectionBound && !isHorizontalDirectionBound) { const { horizontal: { offset: horizontalOffset, }, vertical: { offset: verticalOffset, }, } = checkSnapPoses( moveable, [directionPoses[0][0]], [directionPoses[0][1]], ); [widthDist, heightDist] = getDragDist({ datas, distX: -(isVerticalBound ? verticalBoundOffset : verticalOffset), distY: -(isHorizontalBound ? horizontalBoundOffset : horizontalOffset), }); } else if (isVeritcalDirectionBound) { // left to right, right to left const reversePos = getPosByDirection(poses, [ verticalDirection[0] * -1, verticalDirection[1], ]); const nextDist = solveNextDist( reversePos, verticalPos, verticalBoundInfo!.offset, verticalBoundInfo!.isVertical, false, datas, ); if (!isNaN(nextDist)) { widthDist = nextDist; } const nextPoses = getFixedPoses( matrix, width + direction[0] * widthDist, height + direction[1] * heightDist, fixedPos, direction, is3d, ); heightDist = checkOneWayPos( moveable, [getPosByDirection(nextPoses, direction)], [getPosByDirection(nextPoses, verticalDirection)] , true, datas, ); } else { // top to bottom, bottom to top const reversePos = getPosByDirection(poses, [ horizontalDirection[0] * -1, horizontalDirection[1], ]); const nextDist = solveNextDist( reversePos, verticalPos, horizontalBoundInfo!.offset, horizontalBoundInfo!.isVertical, true, datas, ); if (!isNaN(nextDist)) { heightDist = nextDist; } const nextPoses = getFixedPoses( matrix, width + direction[0] * widthDist, height + direction[1] * heightDist, fixedPos, direction, is3d, ); widthDist = checkOneWayPos( moveable, [getPosByDirection(nextPoses, direction)], [getPosByDirection(nextPoses, horizontalDirection)] , false, datas, ); } return [ direction[0] * widthDist, direction[1] * heightDist, ]; } export function checkSizeDist( moveable: MoveableManager, matrix: number[], width: number, height: number, direction: number[], snapDirection: number[], datas: any, is3d: boolean, ) { const poses = getAbsolutePosesByState(moveable.state); const fixedPos = getPosByReverseDirection(poses, snapDirection); const nextPoses = getFixedPoses(matrix, width, height, fixedPos, direction, is3d); if (direction[0] && direction[1]) { return checkTwoWayDist( moveable, nextPoses, direction, datas, matrix, width, height, fixedPos, is3d, ); } else { return checkOneWayDist(moveable, nextPoses, direction, datas); } } export function checkSnapSize( moveable: MoveableManager, width: number, height: number, direction: number[], datas: any, ) { if (!hasGuidelines(moveable, "resizable")) { return [0, 0]; } const { matrix, is3d, } = moveable.state; return checkSizeDist(moveable, matrix, width, height, direction, direction, datas, is3d); } export function checkSnapScale( moveable: MoveableManager, scale: number[], direction: number[], snapDirection: number[], datas: any, ) { const { width, height, } = datas; if (!hasGuidelines(moveable, "scalable")) { return [0, 0]; } const sizeDist = checkSizeDist( moveable, scaleMatrix(datas, scale), width, height, direction, snapDirection, datas, datas.is3d, ); return [ sizeDist[0] / width, sizeDist[1] / height, ]; } export function solveEquation( pos1: number[], pos2: number[], snapOffset: number, isVertical: boolean, ) { const dx = pos2[0] - pos1[0]; const dy = pos2[1] - pos1[1]; if (!dx) { // y = 0 * x + b // only horizontal if (!isVertical) { return [0, snapOffset]; } return; } if (!dy) { // only vertical if (isVertical) { return [snapOffset, 0]; } return; } // y = ax + b const a = dy / dx; const b = pos1[1] - a * pos1[0]; if (isVertical) { // y = a * x + b const y = a * (pos2[0] + snapOffset) + b; return [snapOffset, y - pos2[1]]; } else { // x = (y - b) / a const x = (pos2[1] + snapOffset - b) / a; return [x - pos2[0], snapOffset]; } } export function getSnapInfosByDirection( moveable: MoveableManager, poses: number[][], snapDirection: number[] | true, ) { if (snapDirection === true) { const rect = getRect(poses); (rect as any).middle = (rect.top + rect.bottom) / 2; (rect as any).center = (rect.left + rect.right) / 2; return checkSnaps(moveable, rect, true, 1); } else if (!snapDirection[0] && !snapDirection[1]) { const alignPoses = [poses[0], poses[1], poses[3], poses[2], poses[0]]; const nextPoses = []; for (let i = 0; i < 4; ++i) { nextPoses.push(alignPoses[i]); poses.push([ (alignPoses[i][0] + alignPoses[i + 1][0]) / 2, (alignPoses[i][1] + alignPoses[i + 1][1]) / 2, ]); } return checkSnapPoses(moveable, nextPoses.map(pos => pos[0]), nextPoses.map(pos => pos[1]), true, 1); } else { const nextPoses = getPosesByDirection(poses, snapDirection); if (nextPoses.length > 1) { nextPoses.push([ (nextPoses[0][0] + nextPoses[1][0]) / 2, (nextPoses[0][1] + nextPoses[1][1]) / 2, ]); } return checkSnapPoses(moveable, nextPoses.map(pos => pos[0]), nextPoses.map(pos => pos[1]), true, 1); } } export function startCheckSnapDrag( moveable: MoveableManager, datas: any, ) { datas.absolutePoses = getAbsolutePosesByState(moveable.state); } export function checkSnapDrag( moveable: MoveableManager, distX: number, distY: number, datas: any, ) { const snapVerticalInfo = { isSnap: false, offset: 0, }; const snapHorizontalInfo = { isSnap: false, offset: 0, }; if (!hasGuidelines(moveable, "draggable")) { return [snapVerticalInfo, snapHorizontalInfo]; } const poses = getAbsolutePoses( datas.absolutePoses, [distX, distY], ); const { left, right, top, bottom } = getRect(poses); const snapInfos = checkSnaps(moveable, { left, right, top, bottom, center: (left + right) / 2, middle: (top + bottom) / 2, }, true); const boundInfos = checkBounds(moveable, [left, right], [top, bottom]); if (boundInfos.vertical.isBound) { snapVerticalInfo.offset = boundInfos.vertical.offset; snapVerticalInfo.isSnap = true; } else if (snapInfos.vertical.isSnap) { // has vertical guidelines snapVerticalInfo.offset = snapInfos.vertical.offset; snapVerticalInfo.isSnap = true; } if (boundInfos.horizontal.isBound) { snapHorizontalInfo.offset = boundInfos.horizontal.offset; snapHorizontalInfo.isSnap = true; } else if (snapInfos.horizontal.isSnap) { // has horizontal guidelines snapHorizontalInfo.offset = snapInfos.horizontal.offset; snapHorizontalInfo.isSnap = true; } return [ snapVerticalInfo, snapHorizontalInfo, ]; } export default { name: "snappable", render(moveable: MoveableManager, React: Renderer): any[] { const { top: targetTop, left: targetLeft, snapDirection, clientRect, containerRect, } = moveable.state; const clientLeft = clientRect.left - containerRect.left; const clientTop = clientRect.top - containerRect.top; // console.log(targetLeft, targetTop); if (!snapDirection || !hasGuidelines(moveable, "")) { return []; } const poses = getAbsolutePosesByState(moveable.state); const { width, height, top, left, bottom, right } = getRect(poses); const { vertical: { guidelines: verticalGuildelines, snapPoses: verticalSnapPoses, }, horizontal: { guidelines: horizontalGuidelines, snapPoses: horizontalSnapPoses, }, } = getSnapInfosByDirection(moveable, poses, snapDirection); const { vertical: { isBound: isVerticalBound, pos: verticalBoundPos, }, horizontal: { isBound: isHorizontalBound, pos: horizontalBoundPos, }, } = checkBounds(moveable, [left, right], [top, bottom], 1); if (isVerticalBound && verticalSnapPoses.indexOf(verticalBoundPos) < 0) { // verticalGuildelines.push({ // type: "vertical", // pos: [verticalBoundPos, top], // size: height, // }); verticalSnapPoses.push(verticalBoundPos); } if (isHorizontalBound && horizontalSnapPoses.indexOf(horizontalBoundPos) < 0) { // horizontalGuidelines.push({ // type: "horizontal", // pos: [left, horizontalBoundPos], // size: width, // }); horizontalSnapPoses.push(horizontalBoundPos); } return [ ...verticalSnapPoses.map((pos, i) => { return
; }), ...horizontalSnapPoses.map((pos, i) => { return
; }), ...verticalGuildelines.map((guideline, i) => { const { pos, size, element } = guideline; return
; }), ...horizontalGuidelines.map((guideline, i) => { const { pos, size, element } = guideline; return
; }), ]; }, dragStart(moveable: MoveableManager, e: any) { moveable.state.snapDirection = true; snapStart(moveable); }, pinchStart(moveable: MoveableManager) { this.unset(moveable); }, dragEnd(moveable: MoveableManager) { this.unset(moveable); }, dragControlCondition: directionCondition, dragControlStart(moveable: MoveableManager, e: any) { moveable.state.snapDirection = null; snapStart(moveable); }, dragControlEnd(moveable: MoveableManager) { this.unset(moveable); }, dragGroupStart(moveable: any, e: any) { moveable.state.snapDirection = true; snapStart(moveable); }, dragGroupEnd(moveable: any) { this.unset(moveable); }, dragGroupControlStart(moveable: any, e: any) { moveable.state.snapDirection = null; snapStart(moveable); }, dragGroupControlEnd(moveable: any) { this.unset(moveable); }, unset(moveable: any) { const state = moveable.state; state.enableSnap = false; state.guidelines = []; state.snapDirection = null; }, };