PromucFlow_constructor/app/client/src/reflow/reflowUtils.ts
Valera Melnikov 6d90ce8dc9
chore: update storybook (#32828)
## Description
- Update Storybook and related dependencies
- Delete the stories for old widget components
- Rewrite stories for new widgets(mdx to tsx)

Note: local chromatic doesn't work because of this
https://github.com/storybookjs/storybook/issues/22531

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced new component stories across various packages to enhance
design system documentation and user experience.

- **Documentation**
- Updated `.gitignore` files to optimize version control settings for
Storybook and build logs.

- **Refactor**
- Modified UI element positioning in Storybook theming for improved
layout management.

- **Style**
- Adjusted CSS properties for the `Select` component to ensure better
visual presentation.

- **Chores**
- Updated dependencies and scripts in package configurations to maintain
up-to-date development tools.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-22 12:17:28 +03:00

2261 lines
67 KiB
TypeScript

import type { OccupiedSpace } from "constants/CanvasEditorConstants";
import { cloneDeep, isUndefined } from "lodash";
import type { Rect } from "utils/boxHelpers";
import { areIntersecting } from "utils/boxHelpers";
import type {
BlockSpace,
CollidingSpace,
CollidingSpaceMap,
CollisionAccessors,
CollisionMap,
CollisionTree,
CollisionTreeCache,
GridProps,
MovementLimitMap,
OrientationAccessors,
PrevReflowState,
ReflowedSpace,
ReflowedSpaceMap,
SecondOrderCollisionMap,
SpaceMap,
SpaceMovementMap,
} from "./reflowTypes";
import {
HORIZONTAL_RESIZE_MIN_LIMIT,
MathComparators,
ReflowDirection,
SpaceAttributes,
VERTICAL_RESIZE_MIN_LIMIT,
} from "./reflowTypes";
/**
* This method is used while calculating MovementMap of the reflowed spaces,
* if a particular space's movement is already calculated once, this method returns a boolean
* if the current calculation should replace the previous calculation
*
* @param oldMovement
* @param newMovement
* @param direction
* @returns boolean
*/
export function shouldReplaceOldMovement(
oldMovement: ReflowedSpace,
newMovement: ReflowedSpace,
direction: ReflowDirection,
) {
if (!oldMovement) return true;
const { directionIndicator, isHorizontal } = getAccessor(direction);
const distanceKey = isHorizontal ? "X" : "Y";
const dimensionKey = isHorizontal ? "width" : "height";
const oldDistance = oldMovement[distanceKey],
newDistance = newMovement[distanceKey];
const oldDimension = oldMovement[dimensionKey] || 0,
newDimension = newMovement[dimensionKey] || 0;
//if either one is undefined and other one is a number, or if both are undefined it should replace
if (
(oldDistance === undefined && newDistance !== undefined) ||
(oldDistance !== undefined && newDistance === undefined) ||
(oldDistance === undefined && newDistance === undefined)
)
return true;
if (oldDistance === undefined || newDistance === undefined) {
return false;
}
// if old movement is in the opposite direction return false
if (oldDistance > 0 !== newDistance > 0) return false;
return newDistance === oldDistance
? compareNumbers(newDimension, oldDimension, directionIndicator > 0)
: compareNumbers(newDistance, oldDistance, directionIndicator > 0);
}
/**
* for calculating all the spaces a particular space is colliding with,
* we have to resize the space through out it's movement in the direction
* for example, if a space is moved by 2 rows in the BOTTOM direction,
* then the space's bottom dimension is increased by 2 rows
*
* @param space
* @param accessors
* @returns resized Dimensions of the space
*/
export function getResizedDimensions(
space: CollidingSpace,
{
direction,
directionIndicator,
parallelMax,
parallelMin,
}: CollisionAccessors,
) {
const reflowedPosition = { ...space, children: [] };
reflowedPosition[direction] =
reflowedPosition.collidingValue +
directionIndicator *
(reflowedPosition[parallelMax] - reflowedPosition[parallelMin]);
return reflowedPosition;
}
/**
* sort the collidingSpaces with respect to the distance from the newSpacePositions
* eg, for the colliding spaces of dragging/moving spaces,
* it is sorted by distance from dragging/moving spaces
*
* @param collidingSpaces
* @param staticPosition
* @param isAscending
*/
export function sortCollidingSpacesByDistance(
collidingSpaces: CollidingSpace[],
isAscending = true,
) {
const distanceComparator = getDistanceComparator(isAscending);
collidingSpaces.sort(distanceComparator);
}
/**
* This is a comparator for colliding spaces to sort them by distance from dragging/moving spaces
*
* @param isAscending
* @returns comparator function
*/
function getDistanceComparator(isAscending = true) {
return function (spaceA: CollidingSpace, spaceB: CollidingSpace) {
const accessorA = getAccessor(spaceA.direction);
const accessorB = getAccessor(spaceB.direction);
const distanceA = Math.abs(
spaceA.collidingValue - spaceA[accessorA.oppositeDirection],
);
const distanceB = Math.abs(
spaceB.collidingValue - spaceB[accessorB.oppositeDirection],
);
return isAscending ? distanceB - distanceA : distanceA - distanceB;
};
}
/**
* Method to generate object map with canHorizontalMove and canVerticalMove for every Dragging/MovingSpace
*
* @param existingMovementLimits
* @param spaceMovementMap
* @param delta
* @param beforeLimit
* @returns object map with canHorizontalMove and canVerticalMove for every Dragging/MovingSpace
*/
export function getShouldReflow(
existingMovementLimits: MovementLimitMap,
spaceMovementMap: SpaceMovementMap | undefined,
delta = { X: 0, Y: 0 },
beforeLimit = false,
) {
if (!spaceMovementMap) return;
const movementKeys = Object.keys(spaceMovementMap || {});
for (const movementKey of movementKeys) {
const spaceMovements = spaceMovementMap[movementKey];
let canHorizontalMove = true,
canVerticalMove = true;
for (const movementLimit of spaceMovements) {
const { coordinateKey, directionalIndicator, isHorizontal, maxMovement } =
movementLimit;
const canMove = compareNumbers(
delta[coordinateKey],
maxMovement,
directionalIndicator < 0,
beforeLimit,
);
if (!canMove) {
if (isHorizontal) canHorizontalMove = false;
else canVerticalMove = false;
}
}
let prevCanHorizontalMove = true,
prevCanVerticalMove = true;
if (existingMovementLimits[movementKey]) {
({
canHorizontalMove: prevCanHorizontalMove,
canVerticalMove: prevCanVerticalMove,
} = existingMovementLimits[movementKey]);
}
existingMovementLimits[movementKey] = {
canVerticalMove: canVerticalMove && prevCanVerticalMove,
canHorizontalMove: canHorizontalMove && prevCanHorizontalMove,
};
}
}
/**
* initializes canHorizontalMove and canVerticalMove as true for all moving/resizing spaces
*
* @param newSpacePositions
* @returns object map with canHorizontalMove and canVerticalMove for every Dragging/MovingSpace
*/
export function initializeMovementLimitMap(
newSpacePositions: OccupiedSpace[],
): MovementLimitMap {
const movementLimitMap: MovementLimitMap = {};
for (const spacePosition of newSpacePositions) {
movementLimitMap[spacePosition.id] = {
canHorizontalMove: true,
canVerticalMove: true,
};
}
return movementLimitMap;
}
/**
* Should return X and Y absolute coordinates of the Dragging/moving spaces relative to the original space positions
*
* @param OGSpacePositionsMap
* @param newSpacePositionsMap
* @param direction
* @returns Object with X, Y
*/
export function getDelta(
OGSpacePositionsMap: SpaceMap,
newSpacePositionsMap: SpaceMap,
direction: ReflowDirection,
) {
const tempId = Object.keys(OGSpacePositionsMap)[0];
const OGSpacePosition = OGSpacePositionsMap[tempId];
const newSpacePosition = newSpacePositionsMap[tempId];
if (!OGSpacePosition || !newSpacePosition) {
return { X: 0, Y: 0 };
}
let X = OGSpacePosition.left - newSpacePosition.left,
Y = OGSpacePosition.top - newSpacePosition.top;
if (direction.indexOf("|") > 0) {
const [verticalDirection, horizontalDirection] = direction.split("|");
const { direction: xDirection } = getAccessor(
horizontalDirection as ReflowDirection,
);
const { direction: yDirection } = getAccessor(
verticalDirection as ReflowDirection,
);
X = OGSpacePosition[xDirection] - newSpacePosition[xDirection];
Y = OGSpacePosition[yDirection] - newSpacePosition[yDirection];
return { X, Y };
}
const { direction: directionalAccessor, isHorizontal } =
getAccessor(direction);
const diff =
OGSpacePosition[directionalAccessor] -
newSpacePosition[directionalAccessor];
if (isHorizontal) X = diff;
else Y = diff;
return { X, Y };
}
/**
* This method checks if the occupied spaces are overlapping with newSpacePositions,
* and generates a map with the overlapping spaces with the direction of collision
*
* @param newSpacePositions Dragging/Moving Spaces
* @param occupiedSpaces
* @param direction
* @param prevCollidingSpaceMap
* @param isHorizontalMove
* @param prevSpacesMap
* @param forceDirection
* @param primaryCollisionMap
* @param shouldReflowDropTarget boolean which indicates if we should reflow drop targets
* @param onTimeout indicates if the reflow is called on timeout
* @param mousePosition mouse Position on canvas grid
* @returns collision spaces Map
*/
export function getCollidingSpaceMap(
newSpacePositions: BlockSpace[],
occupiedSpaces: BlockSpace[],
direction: ReflowDirection,
prevCollidingSpaceMap: CollidingSpaceMap,
isHorizontalMove?: boolean,
prevSpacesMap?: SpaceMap,
forceDirection = false,
primaryCollisionMap?: CollisionMap,
shouldReflowDropTarget = true,
onTimeOut = false,
mousePosition?: OccupiedSpace | undefined,
) {
let isColliding = false;
const collidingSpaceMap: CollisionMap = {};
let order = 1;
const orientationalAccessor = getOrientationAccessor(isHorizontalMove);
const oppositeOrientationalAccessor =
getOrientationAccessor(!isHorizontalMove);
let reflowableOccSpaces = [...occupiedSpaces],
currSpacePositions = [...newSpacePositions];
let shouldRegisterContainerTimeout = false;
//if droptargets are not to be reflowed, resize space positions
// and omit drop targets from the spaces
if (!shouldReflowDropTarget) {
// reset values based on function's result
({
currSpacePositions,
reflowableOccSpaces,
shouldRegisterContainerTimeout,
} = resizeOnContainerCollision(
newSpacePositions,
occupiedSpaces,
mousePosition,
direction,
orientationalAccessor,
prevCollidingSpaceMap,
forceDirection,
isHorizontalMove,
prevSpacesMap,
));
}
for (const newSpacePosition of currSpacePositions) {
for (const occupiedSpace of reflowableOccSpaces) {
if (areOverlapping(occupiedSpace, newSpacePosition)) {
isColliding = true;
const currentSpaceId = occupiedSpace.id;
//sometimes direction mentioned cannot be trusted a direction is intelligently calculated
let movementDirection = getCorrectedDirection(
occupiedSpace,
prevSpacesMap && prevSpacesMap[newSpacePosition.id]
? prevSpacesMap[newSpacePosition.id]
: undefined,
direction,
forceDirection,
prevCollidingSpaceMap && prevCollidingSpaceMap[orientationalAccessor],
isHorizontalMove,
);
// if in case this is a second run of the getMovementMap,
//direction should be same as first or primary run.
if (
primaryCollisionMap &&
primaryCollisionMap[occupiedSpace.id] &&
primaryCollisionMap[occupiedSpace.id].collidingId ===
newSpacePosition.id
) {
movementDirection = primaryCollisionMap[occupiedSpace.id].direction;
}
// if incase of the previous run of the entire reflow algorithm, then even though it might be in
//the opposite orientation of current, we should still consider the previous direction
if (
prevCollidingSpaceMap &&
prevCollidingSpaceMap[oppositeOrientationalAccessor] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][
occupiedSpace.id
] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][occupiedSpace.id]
.collidingId === newSpacePosition.id
) {
movementDirection =
prevCollidingSpaceMap[oppositeOrientationalAccessor][
occupiedSpace.id
].direction;
}
if (occupiedSpace.isDropTarget && onTimeOut && !forceDirection) {
movementDirection = getCollisionDirectionOfDropTarget(
occupiedSpace,
movementDirection,
mousePosition,
);
}
const {
direction: directionAccessor,
directionIndicator,
isHorizontal,
} = getAccessor(movementDirection);
if (isHorizontal !== isHorizontalMove) continue;
// If this particular space is already colliding with another dragging space,
// then the highest in the particular direction will override the lowest value
const currentCollidingSpace = collidingSpaceMap[currentSpaceId];
if (
!currentCollidingSpace ||
(currentCollidingSpace &&
compareNumbers(
newSpacePosition[directionAccessor],
currentCollidingSpace.collidingValue,
directionIndicator > 0,
))
) {
collidingSpaceMap[currentSpaceId] = {
...occupiedSpace,
direction: movementDirection,
collidingValue: newSpacePosition[directionAccessor],
collidingId: newSpacePosition.id,
isHorizontal,
order,
};
order++;
}
}
}
}
return {
isColliding,
collidingSpaceMap,
currSpacePositions,
shouldRegisterContainerTimeout,
};
}
/**
* This method is used while generating a collision tree,
* This checks if the newSpacePosition is overlapping with occupiedSpaces
* and creates a array of those spaces with the direction of collision,
* This is usually the direction passed down but with some exceptions
* based on prevReflowState and isDirectCollidingSpace, it can be different
*
* @param newSpacePosition
* @param OGPosition
* @param globalDirection
* @param direction
* @param gridProps
* @param prevReflowState
* @param globalCollisionMap
* @param occupiedSpaces
* @param occupiedSpaces
* @param isDirectCollidingSpace
* @returns Colliding Spaces Array
*/
export function getCollidingSpacesInDirection(
newSpacePosition: CollidingSpace,
OGPosition: BlockSpace,
globalDirection: ReflowDirection,
direction: ReflowDirection,
gridProps: GridProps,
prevReflowState: PrevReflowState,
globalCollisionMap: CollisionMap,
occupiedSpaces?: BlockSpace[],
isDirectCollidingSpace = false,
) {
const collidingSpaces: CollidingSpace[] = [];
const occupiedSpacesInDirection = filterSpaceByDirection(
newSpacePosition,
occupiedSpaces,
direction,
);
const currentOccupiedSpaces = filterSpaceByDirection(
newSpacePosition,
occupiedSpaces,
getOppositeDirection(direction),
);
const accessor = getAccessor(direction);
const { prevMovementMap, prevSecondOrderCollisionMap } = prevReflowState;
let order = 1;
for (const occupiedSpace of currentOccupiedSpaces) {
// determines if the space acn be added to the list of colliding spaces, if so in what direction
const { changedDirection, collidingValue, isHorizontal, shouldAddToArray } =
ShouldAddToCollisionSpacesArray(
newSpacePosition,
OGPosition,
occupiedSpace,
direction,
accessor,
isDirectCollidingSpace,
gridProps,
globalDirection,
prevMovementMap,
prevSecondOrderCollisionMap,
);
let currentDirection = direction,
currentCollidingValue = newSpacePosition[accessor.direction],
isCurrentHorizontal = accessor.isHorizontal;
if (collidingValue !== undefined && changedDirection) {
currentDirection = changedDirection;
currentCollidingValue = collidingValue;
isCurrentHorizontal = !!isHorizontal;
}
const currentAccessor = getAccessor(currentDirection);
const collidingSpace = globalCollisionMap[occupiedSpace.id];
// If the space already collides with a moving/dragging space and it's collisionValue is farther than current's Then don't add.
if (
collidingSpace &&
collidingSpace.direction === currentDirection &&
compareNumbers(
collidingSpace.collidingValue,
currentCollidingValue,
currentAccessor.directionIndicator > 0,
)
)
continue;
if (shouldAddToArray) {
collidingSpaces.push({
...occupiedSpace,
direction: currentDirection,
collidingValue: currentCollidingValue,
collidingId: newSpacePosition.id,
isHorizontal: isCurrentHorizontal,
order,
});
order++;
}
}
return {
collidingSpaces,
occupiedSpacesInDirection,
skipCollisionTree: false,
};
}
/**
* Checks if the collidingSpace is overlapping with newSpacePosition
* and check if collidingSpace should be added to the collidingSpaceArray
* and return and object with boolean and other variables if there is a change in direction
*
* @param newSpacePosition
* @param OGPosition
* @param collidingSpace
* @param direction
* @param accessor
* @param isDirectCollidingSpace
* @param gridProps
* @param globalDirection
* @param prevMovementMap
* @param prevSecondOrderCollisionMap
* @returns object with boolean if the occupied space is to be added to array, also mentions if there is a change in direction
*/
export function ShouldAddToCollisionSpacesArray(
newSpacePosition: CollidingSpace,
OGPosition: BlockSpace,
collidingSpace: OccupiedSpace,
direction: ReflowDirection,
accessor: CollisionAccessors,
isDirectCollidingSpace: boolean,
gridProps: GridProps,
globalDirection: ReflowDirection,
prevMovementMap?: ReflowedSpaceMap,
prevSecondOrderCollisionMap?: SecondOrderCollisionMap,
) {
// not intersecting then dont add to array
if (!areOverlapping(collidingSpace, newSpacePosition))
return { shouldAddToArray: false };
const {
direction: directionAccessor,
directionIndicator,
isHorizontal,
oppositeDirection,
parallelMax,
parallelMin,
} = accessor;
const prevCollisionMap: {
children: {
[key: string]: any;
};
} = (prevSecondOrderCollisionMap &&
prevSecondOrderCollisionMap[newSpacePosition.id]) || {
children: {},
};
// if these two spaces previously collided then it calculates how it collided
if (prevCollisionMap.children[collidingSpace.id]) {
return getCollisionStatusBasedOnPrevValue(
newSpacePosition,
collidingSpace,
direction,
accessor,
gridProps,
prevCollisionMap,
prevMovementMap,
);
}
const movementDirectionAccessor = accessor.isHorizontal
? "directionX"
: "directionY";
// if it collided in the previous run in this particular orientation the previous direction is considered
if (
prevMovementMap &&
prevMovementMap[collidingSpace.id] &&
prevMovementMap[collidingSpace.id][movementDirectionAccessor]
) {
const prevDirection =
prevMovementMap[collidingSpace.id][movementDirectionAccessor];
const shouldAddToArray = prevDirection === direction;
return { shouldAddToArray };
}
const reflowedSpacePosition = {
...newSpacePosition,
[accessor.oppositeDirection]: newSpacePosition.collidingValue,
};
// if it does not collide in it's current reflowed position then it should not be added to the branch
if (
!isDirectCollidingSpace &&
globalDirection !== direction &&
!areOverlapping(reflowedSpacePosition, collidingSpace)
)
return { shouldAddToArray: false };
// if this particular space does not collide directly with dragging spaces then can directly be added to list
if (!isDirectCollidingSpace) return { shouldAddToArray: true };
// next to condition return true if they dont have enough data to move forward because it is the first time reflow algorithm is run
if (!prevSecondOrderCollisionMap) return { shouldAddToArray: true };
if (!prevMovementMap || !prevMovementMap[newSpacePosition.id])
return { shouldAddToArray: true };
const { [OGPosition.id]: prevStaticSpace } = getModifiedOccupiedSpacesMap(
{ [OGPosition.id]: { ...OGPosition } },
prevMovementMap,
!isHorizontal,
gridProps,
parallelMax,
parallelMin,
);
//determines if should be added by comparing previous value to now,
// for example if previous bottom of dragging space is lesser than top of colliding space,
//then it should collide in the bottom direction
const shouldAddToArray = compareNumbers(
collidingSpace[oppositeDirection],
prevStaticSpace[directionAccessor],
directionIndicator > 0,
true,
);
const currentStaticSpace = {
...newSpacePosition,
[oppositeDirection]: newSpacePosition.collidingValue,
};
const correctedDirection = getCorrectedDirection(
collidingSpace,
prevStaticSpace,
globalDirection,
);
const correctedAccessor = getAccessor(correctedDirection);
// if this cant be added in mentioned direction but still intersects, then we determine the correct direction
// but if it says the currentDirection is same as old one, it should not add
if (
!shouldAddToArray &&
areOverlapping(collidingSpace, currentStaticSpace) &&
correctedDirection !== direction &&
correctedAccessor.isHorizontal !== isHorizontal
) {
let collidingValue = newSpacePosition.collidingValue;
if (isHorizontal !== correctedAccessor.isHorizontal) {
collidingValue = currentStaticSpace[correctedAccessor.direction];
}
return {
shouldAddToArray: true,
changedDirection: correctedDirection,
collidingValue: collidingValue,
isHorizontal: correctedAccessor.isHorizontal,
};
}
return { shouldAddToArray };
}
/**
* method to filter occupiedSpaces to be after a newSpacePosition in direction
* eg, if the direction is BOTTOM, this methods returns all the occupiedSpaces below the newSpacePosition
*
* @param newSpacePosition
* @param occupiedSpaces
* @param direction
* @returns filtered array of occupied space
*/
export function filterSpaceByDirection(
newSpacePosition: BlockSpace,
occupiedSpaces: BlockSpace[] | undefined,
direction: ReflowDirection,
): BlockSpace[] {
let filteredSpaces: BlockSpace[] = [];
const {
direction: directionAccessor,
directionIndicator,
oppositeDirection,
} = getAccessor(direction);
if (occupiedSpaces) {
filteredSpaces = occupiedSpaces.filter((occupiedSpace) => {
if (
occupiedSpace.id === newSpacePosition.id ||
occupiedSpace.parentId === newSpacePosition.id
) {
return false;
}
return compareNumbers(
occupiedSpace[directionAccessor],
newSpacePosition[oppositeDirection],
directionIndicator > 0,
);
});
}
return filteredSpaces;
}
/**
* filters out occupiedSpaces, and returns array without a space with id
*
* @param id
* @param occupiedSpaces
* @returns filtered occupied spaces
*/
export function filterSpaceById(
id: string,
occupiedSpaces: OccupiedSpace[] | undefined,
): OccupiedSpace[] {
let filteredSpaces: OccupiedSpace[] = [];
if (occupiedSpaces) {
filteredSpaces = occupiedSpaces.filter((occupiedSpace) => {
return occupiedSpace.id !== id && occupiedSpace.parentId !== id;
});
}
return filteredSpaces;
}
/**
* filters out occupiedSpaceMap and removes spaces with ids of newSpacePositionsMap
*
* @param newSpacePositionsMap
* @param occupiedSpaceMap
* @mutates occupiedSpaceMap
*/
export function filterCommonSpaces(
newSpacePositionsMap: { [key: string]: any },
occupiedSpaceMap: SpaceMap,
) {
const keysToFilter = Object.keys(newSpacePositionsMap);
for (const key of keysToFilter) {
if (occupiedSpaceMap[key]) {
delete occupiedSpaceMap[key];
}
}
}
/**
* easily the most important method in the algorithm
*
* @param r1 space1
* @param r2 space 2
* @returns boolean if it is colliding
*/
function areOverlapping(r1: Rect, r2: Rect) {
return !(
r2.left >= r1.right ||
r2.right <= r1.left ||
r2.top >= r1.bottom ||
r2.bottom <= r1.top
);
}
/**
* This method checks if collidingSpace can collide
* with the moving/dragging spaces in a particular direction
* if they cant collide in that direction a direction is determined based on prevPositions
*
* @param collidingSpace
* @param prevPositions
* @param direction
* @param forceDirection
* @param prevCollidingSpaces
* @param isHorizontalMove
* @returns direction
*/
function getCorrectedDirection(
collidingSpace: OccupiedSpace,
prevPositions: OccupiedSpace | undefined,
direction: ReflowDirection,
forceDirection = false,
prevCollidingSpaces?: CollisionMap,
isHorizontalMove?: boolean,
): ReflowDirection {
if (forceDirection) return direction;
// if previously collided in that direction then it should be that direction
if (prevCollidingSpaces && prevCollidingSpaces[collidingSpace.id]) {
return prevCollidingSpaces[collidingSpace.id].direction;
}
let primaryDirection: ReflowDirection = direction,
secondaryDirection: ReflowDirection | undefined = undefined;
// this is for composite directions for resizing while dragging the corner handles
if (direction.indexOf("|") >= 0) {
const directions = direction.split("|");
if (isHorizontalMove) {
primaryDirection = directions[1] as ReflowDirection;
secondaryDirection = directions[0] as ReflowDirection;
} else {
primaryDirection = directions[0] as ReflowDirection;
secondaryDirection = directions[1] as ReflowDirection;
}
}
// if first run the return direction
if (!prevPositions) return primaryDirection;
const primaryAccessors = getAccessor(primaryDirection);
// check if they can collide based on previous location of the dragging space
const isCorrectDirection = compareNumbers(
collidingSpace[primaryAccessors.oppositeDirection],
prevPositions[primaryAccessors.direction],
primaryAccessors.directionIndicator > 0,
true,
);
if (isCorrectDirection) {
return primaryDirection;
} else if (secondaryDirection) {
return secondaryDirection;
}
//if all the conditions doesn't match then we have to determine manually
return getVerifiedDirection(
collidingSpace,
prevPositions,
primaryDirection,
primaryAccessors.isHorizontal,
);
}
/**
* if spaces cannot possibly collide in certain direction,
* this method provides the direction it is most likely to collide in
* This is determined by checking the previous positions of the dragging/moving spaces
* with the dimension of collidingSpace
*
* @param collidingSpace
* @param prevPositions
* @param direction
* @param isHorizontalMove
* @returns direction
*/
function getVerifiedDirection(
collidingSpace: OccupiedSpace,
prevPositions: OccupiedSpace,
direction: ReflowDirection,
isHorizontalMove: boolean,
) {
// determines direction by comparing if it can collide in all the directions
if (isHorizontalMove) {
if (collidingSpace.bottom <= prevPositions.top) {
return ReflowDirection.TOP;
} else if (collidingSpace.top >= prevPositions.bottom) {
return ReflowDirection.BOTTOM;
} else if (
direction !== ReflowDirection.RIGHT &&
collidingSpace.left >= prevPositions.right
) {
return ReflowDirection.RIGHT;
} else if (
direction !== ReflowDirection.LEFT &&
collidingSpace.right <= prevPositions.left
) {
return ReflowDirection.LEFT;
}
} else {
if (collidingSpace.right <= prevPositions.left) {
return ReflowDirection.LEFT;
} else if (collidingSpace.left >= prevPositions.right) {
return ReflowDirection.RIGHT;
} else if (
direction !== ReflowDirection.TOP &&
collidingSpace.bottom <= prevPositions.top
) {
return ReflowDirection.TOP;
} else if (
direction !== ReflowDirection.BOTTOM &&
collidingSpace.top >= prevPositions.bottom
) {
return ReflowDirection.BOTTOM;
}
}
return direction;
}
/**
* compares numbers and returns boolean
* @param numberA
* @param numberB
* @param isGreaterThan
* @param isEqual
* @returns boolean
*/
export function compareNumbers(
numberA: number,
numberB: number,
isGreaterThan: boolean,
isEqual = false,
): boolean {
if (isGreaterThan) {
if (isEqual) {
return numberA >= numberB;
}
return numberA > numberB;
}
if (isEqual) {
return numberA <= numberB;
}
return numberA < numberB;
}
/**
* gets opposite direction
* @param direction
* @returns ReflowDirection
*/
export function getOppositeDirection(
direction: ReflowDirection,
): ReflowDirection {
const directionalAccessors = getAccessor(direction);
return directionalAccessors.oppositeDirection.toUpperCase() as ReflowDirection;
}
/**
* Accessors are used to access space's dimension based on direction
* These are string accessors to get the dimension of the space in a direction
*
* @param direction
* @returns accessors
*/
export function getAccessor(direction: ReflowDirection): CollisionAccessors {
switch (direction) {
case ReflowDirection.LEFT:
return {
direction: SpaceAttributes.left,
oppositeDirection: SpaceAttributes.right,
perpendicularMax: SpaceAttributes.bottom,
perpendicularMin: SpaceAttributes.top,
parallelMax: SpaceAttributes.right,
parallelMin: SpaceAttributes.left,
mathComparator: MathComparators.max,
oppositeMathComparator: MathComparators.min,
directionIndicator: -1,
isHorizontal: true,
plane: "horizontal",
};
case ReflowDirection.RIGHT:
return {
direction: SpaceAttributes.right,
oppositeDirection: SpaceAttributes.left,
perpendicularMax: SpaceAttributes.bottom,
perpendicularMin: SpaceAttributes.top,
parallelMax: SpaceAttributes.right,
parallelMin: SpaceAttributes.left,
mathComparator: MathComparators.min,
oppositeMathComparator: MathComparators.max,
directionIndicator: 1,
isHorizontal: true,
plane: "horizontal",
};
case ReflowDirection.TOP:
return {
direction: SpaceAttributes.top,
oppositeDirection: SpaceAttributes.bottom,
perpendicularMax: SpaceAttributes.right,
perpendicularMin: SpaceAttributes.left,
parallelMax: SpaceAttributes.bottom,
parallelMin: SpaceAttributes.top,
mathComparator: MathComparators.max,
oppositeMathComparator: MathComparators.min,
directionIndicator: -1,
isHorizontal: false,
plane: "vertical",
};
case ReflowDirection.BOTTOM:
return {
direction: SpaceAttributes.bottom,
oppositeDirection: SpaceAttributes.top,
perpendicularMax: SpaceAttributes.right,
perpendicularMin: SpaceAttributes.left,
parallelMax: SpaceAttributes.bottom,
parallelMin: SpaceAttributes.top,
mathComparator: MathComparators.min,
oppositeMathComparator: MathComparators.max,
directionIndicator: 1,
isHorizontal: false,
plane: "vertical",
};
}
return {
direction: SpaceAttributes.bottom,
oppositeDirection: SpaceAttributes.top,
perpendicularMax: SpaceAttributes.right,
perpendicularMin: SpaceAttributes.left,
parallelMax: SpaceAttributes.bottom,
parallelMin: SpaceAttributes.top,
mathComparator: MathComparators.min,
oppositeMathComparator: MathComparators.max,
directionIndicator: 1,
isHorizontal: false,
plane: "vertical",
};
}
/**
* get Max X coordinate of the the space,
* MaxX is the maximum a reflowed space can move in the X axis before it should start to resize,
*
* @param collisionTree
* @param gridProps
* @param direction
* @param occupiedLength
* @param maxOccupiedSpace
* @param shouldResize
* @returns number
*/
export function getMaxX(
collisionTree: CollisionTree,
gridProps: GridProps,
direction: ReflowDirection,
occupiedLength: number,
maxOccupiedSpace: number,
shouldResize: boolean,
) {
const accessors = getAccessor(direction);
const movementLimit = shouldResize ? occupiedLength : maxOccupiedSpace;
let maxX = collisionTree[accessors.direction] - movementLimit;
if (direction === ReflowDirection.RIGHT) {
maxX =
gridProps.maxGridColumns -
collisionTree[accessors.direction] -
movementLimit;
}
return accessors.directionIndicator * maxX * gridProps.parentColumnSpace;
}
/**
* get Max Y coordinate of the the space
* MaxY is the maximum a reflowed space can move in the Y axis before it should start to resize,
*
* @param collisionTree
* @param gridProps
* @param direction
* @param occupiedLength
* @param maxOccupiedSpace
* @param shouldResize
* @returns number
*/
export function getMaxY(
collisionTree: CollisionTree,
gridProps: GridProps,
direction: ReflowDirection,
occupiedLength: number,
maxOccupiedSpace: number,
shouldResize: boolean,
) {
const accessors = getAccessor(direction);
const movementLimit = shouldResize ? occupiedLength : maxOccupiedSpace;
let maxY =
(collisionTree[accessors.direction] - movementLimit) *
gridProps.parentRowSpace;
if (direction === ReflowDirection.BOTTOM) {
maxY = Infinity;
}
return accessors.directionIndicator * maxY;
}
/**
* calculates the reflowed distance i.e, X or Y of the reflowed space
* this distance indicates the absolute value by which the reflowed space has to move rom it's original position
*
* @param collisionTree
* @param direction
* @param maxDistance
* @param distanceBeforeCollision
* @param actualDimension
* @param emptySpaces
* @param snapGridSpace
* @param expandableCanvas
* @returns distance in number
*/
export function getReflowDistance(
collisionTree: CollisionTree,
direction: ReflowDirection,
maxDistance: number,
distanceBeforeCollision: number,
actualDimension: number,
emptySpaces: number,
snapGridSpace: number,
expandableCanvas = false,
) {
const accessors = getAccessor(direction);
const originalDimension =
(collisionTree[accessors.parallelMax] -
collisionTree[accessors.parallelMin]) *
snapGridSpace;
const value =
(distanceBeforeCollision + emptySpaces * accessors.directionIndicator) *
snapGridSpace *
-1;
const maxValue = Math[accessors.mathComparator](value, maxDistance);
if (expandableCanvas) {
return maxValue;
}
return accessors.directionIndicator < 0
? maxValue
: maxValue + originalDimension - actualDimension;
}
/**
* returns the reflowed dimension (width or height) of the reflowed space
* It returns the original dimension if it has not reached the canvas borders
*
* @param collisionTree
* @param direction
* @param travelDistance
* @param maxDistance
* @param distanceBeforeCollision
* @param snapGridSpace
* @param emptySpaces
* @param minDimension
* @param shouldResize
* @returns resized width or height of space
*/
export function getReflowedDimension(
collisionTree: CollisionTree,
direction: ReflowDirection,
travelDistance: number,
maxDistance: number,
distanceBeforeCollision: number,
snapGridSpace: number,
emptySpaces: number,
minDimension: number,
shouldResize: boolean,
) {
const accessors = getAccessor(direction);
if (direction === ReflowDirection.TOP && collisionTree.fixedHeight)
return collisionTree.fixedHeight * snapGridSpace;
const currentDistanceBeforeCollision =
travelDistance +
(distanceBeforeCollision + emptySpaces * accessors.directionIndicator) *
snapGridSpace;
const originalDimension =
collisionTree[accessors.parallelMax] - collisionTree[accessors.parallelMin];
if (!shouldResize) {
return originalDimension * snapGridSpace;
}
const resizeThreshold = maxDistance + currentDistanceBeforeCollision;
const resizeLimit =
resizeThreshold +
(originalDimension - minDimension) *
snapGridSpace *
accessors.directionIndicator;
let shrink = 0;
const canResize = compareNumbers(
travelDistance,
resizeThreshold,
accessors.directionIndicator > 0,
true,
);
if (canResize) {
shrink = Math[accessors.mathComparator](travelDistance, resizeLimit);
shrink = shrink - resizeThreshold;
}
return originalDimension * snapGridSpace - Math.abs(shrink);
}
/**
* check the limits of each movement map
* and replace with previous run's movement values if it has already reached the limit
*
* @param movementMap
* @param prevMovementMap
* @param movementLimit
* @returns
*/
export function getLimitedMovementMap(
movementMap: ReflowedSpaceMap | undefined,
prevMovementMap: ReflowedSpaceMap,
movementLimit: { canVerticalMove: boolean; canHorizontalMove: boolean },
): ReflowedSpaceMap {
if (!movementMap) return {};
const { canHorizontalMove, canVerticalMove } = movementLimit;
if (!canVerticalMove && !canHorizontalMove) {
return prevMovementMap;
}
if (!canVerticalMove) {
return replaceMovementMapByDirection(movementMap, prevMovementMap, false);
}
if (!canHorizontalMove) {
return replaceMovementMapByDirection(movementMap, prevMovementMap, true);
}
return movementMap;
}
/**
* replace movement of the reflowed space with previous run's movement of the reflowed space
*
* @param movementMap
* @param prevMovementMap
* @param replaceHorizontal
* @returns
*/
function replaceMovementMapByDirection(
movementMap: ReflowedSpaceMap,
prevMovementMap: ReflowedSpaceMap,
replaceHorizontal: boolean,
): ReflowedSpaceMap {
const checkKey = replaceHorizontal ? "X" : "Y";
const currentMovementMap = { ...movementMap };
const movementMapIds = Object.keys(movementMap);
for (const spaceId of movementMapIds) {
if (currentMovementMap[spaceId][checkKey] !== undefined) {
delete currentMovementMap[spaceId];
if (prevMovementMap[spaceId]) {
currentMovementMap[spaceId] = { ...prevMovementMap[spaceId] };
}
}
}
return currentMovementMap;
}
/**
* on Container exit, the exited container and the widgets behind it should reflow in opposite direction,
* So this method checks if that it the case and sets it in the opposite direction
*
* @param collidingSpaceMap
* @param exitContainerId
* @param mousePosition mouse Position on canvas grid
* @param spacePositionMap
* @param direction
* changes reference of collidingSpaceMap
*/
export function changeExitContainerDirection(
collidingSpaceMap: CollisionMap,
exitContainerId: string | undefined,
mousePosition: OccupiedSpace | undefined,
spacePositionMap: SpaceMap,
) {
if (
!exitContainerId ||
!collidingSpaceMap[exitContainerId] ||
!mousePosition
) {
return;
}
const exitEdgeDirection = getContainerExitEdge(
collidingSpaceMap[exitContainerId],
mousePosition,
);
if (!exitEdgeDirection) return;
const {
direction: directionAccessor,
directionIndicator,
oppositeDirection: exitDirectionAccessor,
} = getAccessor(exitEdgeDirection);
const collidingSpaces: CollidingSpace[] = Object.values(collidingSpaceMap);
const oppositeFrom =
collidingSpaceMap[exitContainerId][exitDirectionAccessor];
const oppositeSpaceIds = collidingSpaces
.filter((collidingSpace: CollidingSpace) => {
return compareNumbers(
collidingSpace[exitDirectionAccessor],
oppositeFrom,
directionIndicator > 0,
true,
);
})
.map((collidingSpace: CollidingSpace) => collidingSpace.id);
for (const spaceId of oppositeSpaceIds) {
collidingSpaceMap[spaceId].direction = exitEdgeDirection;
collidingSpaceMap[spaceId].collidingValue =
spacePositionMap[collidingSpaceMap[spaceId].collidingId][
directionAccessor
];
}
collidingSpaceMap[exitContainerId].direction =
getOppositeDirection(exitEdgeDirection);
collidingSpaceMap[exitContainerId].collidingValue =
spacePositionMap[collidingSpaceMap[exitContainerId].collidingId][
exitDirectionAccessor
];
}
/**
* Convert an array of spaces to map of spaces
*
* @param spacesArray
* @returns space map
*/
export function getSpacesMapFromArray(spacesArray: BlockSpace[] | undefined) {
if (!spacesArray) return {};
const spacesMap: SpaceMap = {};
for (const space of spacesArray) {
spacesMap[space.id] = space;
}
return spacesMap;
}
/**
* build a collision array to Collision map structure
*
* @param spacesArray
* @returns space map
*/
export function buildArrayToCollisionMap(
collidingSpaces: CollidingSpace[] | undefined,
) {
if (!collidingSpaces) return {};
const collidingSpaceMap: CollisionMap = {};
let order = 1;
for (const collidingSpace of collidingSpaces) {
const { directionIndicator } = getAccessor(collidingSpace.direction);
const prevCollidingSpace = collidingSpaceMap[collidingSpace.id];
if (
!prevCollidingSpace ||
(prevCollidingSpace &&
prevCollidingSpace.direction === collidingSpace.direction &&
compareNumbers(
collidingSpace.collidingValue,
prevCollidingSpace.collidingValue,
directionIndicator > 0,
))
) {
collidingSpaceMap[collidingSpace.id] = { ...collidingSpace, order };
order++;
}
}
return collidingSpaceMap;
}
/**
* move the occupied spaces to previous opposite orientation run's position only in a particular orientation
* eg, if the current orientation is horizontal, then the occupiedSpacesMap's top and bottom positions are modified
* to match previously reflowed values
*
* @param occupiedSpacesMap
* @param prevMovementMap
* @param isHorizontal
* @param gridProps
* @param directionMax
* @param directionMin
* @returns modified occupied space map
*/
export function getModifiedOccupiedSpacesMap(
occupiedSpacesMap: SpaceMap,
prevMovementMap: ReflowedSpaceMap | undefined,
isHorizontal: boolean,
gridProps: GridProps,
directionMax: SpaceAttributes,
directionMin: SpaceAttributes,
) {
if (!prevMovementMap) return cloneDeep(occupiedSpacesMap);
const spaceKeys = Object.keys(occupiedSpacesMap);
const directionalOccupiedSpacesMap: SpaceMap = {};
const displaceMentAccessor = isHorizontal ? "Y" : "X";
const dimensionAccessor = isHorizontal ? "height" : "width";
const gridGap = isHorizontal
? gridProps.parentRowSpace
: gridProps.parentColumnSpace;
for (const key of spaceKeys) {
const movement =
(prevMovementMap[key] && prevMovementMap[key][displaceMentAccessor]) || 0;
const dimension =
prevMovementMap[key] && prevMovementMap[key][dimensionAccessor];
const currentSpace = occupiedSpacesMap[key];
directionalOccupiedSpacesMap[key] = {
...currentSpace,
[directionMin]:
currentSpace[directionMin] + Math.round(movement / gridGap),
[directionMax]: dimension
? currentSpace[directionMin] +
Math.round((movement + dimension) / gridGap)
: currentSpace[directionMax] + Math.round(movement / gridGap),
};
}
return directionalOccupiedSpacesMap;
}
/**
* Modifies a single CollidingSpace to previous opposite orientation run's position only in a particular orientation
* eg, if the current orientation is horizontal, then the collidingSpace's top and bottom positions are modified
* to match previously reflowed values
*
* @param collidingSpace
* @param OGOccupiedSpacesMap
* @param prevMovementMap
* @param isHorizontal
* @param gridProps
* @param directionMax
* @param directionMin
* @returns modified collidingSpace positions
*/
export function getModifiedCollidingSpace(
collidingSpace: CollidingSpace,
OGOccupiedSpacesMap: SpaceMap,
prevMovementMap: ReflowedSpaceMap | undefined,
isHorizontal: boolean,
gridProps: GridProps,
directionMax: SpaceAttributes,
directionMin: SpaceAttributes,
) {
if (!prevMovementMap) return { ...collidingSpace };
const displaceMentAccessor = isHorizontal ? "Y" : "X";
const dimensionAccessor = isHorizontal ? "height" : "width";
const gridGap = isHorizontal
? gridProps.parentRowSpace
: gridProps.parentColumnSpace;
const spaceId = collidingSpace.id;
const OGCollidingSpacePosition = OGOccupiedSpacesMap[spaceId];
const movement =
(prevMovementMap[spaceId] &&
prevMovementMap[spaceId][displaceMentAccessor]) ||
0;
const dimension =
prevMovementMap[spaceId] && prevMovementMap[spaceId][dimensionAccessor];
const currentCollidingSpace = {
...collidingSpace,
[directionMin]:
OGCollidingSpacePosition[directionMin] + Math.round(movement / gridGap),
[directionMax]: dimension
? OGCollidingSpacePosition[directionMin] +
Math.round((movement + dimension) / gridGap)
: OGCollidingSpacePosition[directionMax] + Math.round(movement / gridGap),
};
return currentCollidingSpace;
}
/**
* Check if the new CollidingSpaces are colliding with the new Space positions,
* if so it is to be added to the Collision map of that new Space position's colliding map
*
* @param collidingSpace
* @param OGCollidingSpacePosition
* @param globalDirection
* @param direction
* @param newSpacePositions
* @param globalCollidingSpaces
* @param insertionIndex
* @param globalProcessedNodes
* @param collidingSpaceMap
* @param prevReflowState
* @param isSecondRun boolean to indicate if it is being run for the second time
* @returns boolean to stop moving any further
*/
export function checkReCollisionWithOtherNewSpacePositions(
collidingSpace: CollidingSpace,
OGCollidingSpacePosition: BlockSpace,
globalDirection: ReflowDirection,
direction: ReflowDirection,
newSpacePositions: BlockSpace[],
globalCollidingSpaces: CollidingSpace[],
insertionIndex: number,
globalProcessedNodes: CollisionTreeCache,
collidingSpaceMap: CollisionMap,
prevReflowState: PrevReflowState,
isSecondRun: boolean,
): boolean {
const accessor = getAccessor(direction);
const { isHorizontal: globalIsHorizontal } = getAccessor(globalDirection);
const { prevCollidingSpaceMap, prevSpacesMap } = prevReflowState;
const orientationalAccessor = getOrientationAccessor(globalIsHorizontal);
const oppositeOrientationalAccessor =
getOrientationAccessor(!globalIsHorizontal);
let stopCollisionCheck = false;
for (const newSpacePosition of newSpacePositions) {
if (areOverlapping(newSpacePosition, collidingSpace)) {
//If it is already colliding directly with a moving/Dragging space then no need to check
if (
collidingSpaceMap[collidingSpace.id] &&
collidingSpaceMap[collidingSpace.id].collidingId === newSpacePosition.id
)
continue;
let currentDirection = getCorrectedDirection(
collidingSpace,
prevSpacesMap && prevSpacesMap[newSpacePosition.id],
globalDirection,
false,
prevCollidingSpaceMap && prevCollidingSpaceMap[orientationalAccessor],
globalIsHorizontal,
);
//this is to check if the same two widgets collide again it should be in the same direction even if they have opposite orientation
if (
prevCollidingSpaceMap &&
prevCollidingSpaceMap[oppositeOrientationalAccessor] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][
collidingSpace.id
] &&
prevCollidingSpaceMap[oppositeOrientationalAccessor][collidingSpace.id]
.collidingId === newSpacePosition.id
) {
currentDirection =
prevCollidingSpaceMap[oppositeOrientationalAccessor][
collidingSpace.id
].direction;
}
//if this is being run for the second orientation, no matter what direction is predicted,
//it has to collide in the passed direction
if (isSecondRun) currentDirection = direction;
const currentAccessors = getAccessor(currentDirection);
const currentCollidingSpace: CollidingSpace = {
...OGCollidingSpacePosition,
direction: currentDirection,
collidingId: newSpacePosition.id,
collidingValue: newSpacePosition[currentAccessors.direction],
isHorizontal: currentAccessors.isHorizontal,
order: 0,
};
if (
currentDirection === direction &&
compareNumbers(
currentCollidingSpace.collidingValue,
collidingSpace.collidingValue,
accessor.directionIndicator > 0,
)
) {
stopCollisionCheck = true;
collidingSpaceMap[collidingSpace.id] = currentCollidingSpace;
globalCollidingSpaces.splice(
insertionIndex + 1,
0,
currentCollidingSpace,
);
delete globalProcessedNodes[collidingSpace.id];
}
}
}
return stopCollisionCheck;
}
/**
* If exact same spaces collide with each other again in the current run and previous run
* calculate the direction they collide in
*
* @param staticSpace
* @param collidingSpace
* @param direction
* @param accessor
* @param gridProps
* @param prevCollisionMap
* @param prevMovementMap
* @returns
*/
function getCollisionStatusBasedOnPrevValue(
staticSpace: CollidingSpace,
collidingSpace: OccupiedSpace,
direction: ReflowDirection,
accessor: CollisionAccessors,
gridProps: GridProps,
prevCollisionMap: {
children: {
[key: string]: any;
};
},
prevMovementMap: ReflowedSpaceMap | undefined,
): {
shouldAddToArray: boolean;
changedDirection?: ReflowDirection;
collidingValue?: number;
isHorizontal?: boolean;
} {
const prevCollisionSpace = prevCollisionMap.children[collidingSpace.id];
const reflowedSpacePosition = {
...staticSpace,
[accessor.oppositeDirection]: staticSpace.collidingValue,
};
// If it previously as well collided in the current direction, then add to the Colliding spaces List
if (prevCollisionSpace.direction === direction) {
return { shouldAddToArray: true };
} //if it is previously collided in the same orientation but current colliding value is lesser the dont add
else if (
prevCollisionSpace.isHorizontal === accessor.isHorizontal &&
compareNumbers(
staticSpace.collidingValue,
collidingSpace[accessor.direction],
accessor.directionIndicator > 0,
)
) {
return {
shouldAddToArray: false,
};
} //if it is previously collided in the same orientation but current colliding value is greater then add in the previous direction
else if (prevCollisionSpace.isHorizontal === accessor.isHorizontal) {
return {
shouldAddToArray: true,
changedDirection: prevCollisionSpace.direction,
collidingValue: staticSpace.collidingValue,
isHorizontal: prevCollisionSpace.isHorizontal,
};
} else if (!areOverlapping(reflowedSpacePosition, collidingSpace)) {
return { shouldAddToArray: false };
} else {
const localAccessor = getAccessor(prevCollisionSpace.direction);
const dimensionAccessor = localAccessor.isHorizontal ? "width" : "height";
const gridGap = localAccessor.isHorizontal
? gridProps.parentColumnSpace
: gridProps.parentRowSpace;
const dimension =
prevMovementMap &&
prevMovementMap[staticSpace.id] &&
prevMovementMap[staticSpace.id][dimensionAccessor] !== undefined
? Math.round(
(prevMovementMap[staticSpace.id][dimensionAccessor] || 0) / gridGap,
)
: staticSpace[localAccessor.parallelMax] -
staticSpace[localAccessor.parallelMin];
const orientationalDimension = localAccessor.directionIndicator * dimension;
return {
shouldAddToArray: true,
changedDirection: prevCollisionSpace.direction,
collidingValue:
staticSpace[localAccessor.oppositeDirection] + orientationalDimension,
isHorizontal: prevCollisionSpace.isHorizontal,
};
}
}
/**
* get orientation accessor for first and second run
* @param isHorizontal
* @returns orientation object
*/
export function getOrientationAccessors(
isHorizontal: boolean,
): OrientationAccessors {
return isHorizontal
? { primary: "horizontal", secondary: "vertical" }
: { primary: "vertical", secondary: "horizontal" };
}
/**
* get maximum and minimum space attributes in both orientation
* eg, if primary orientation is horizontal the,
* primary max is bottom, min is top and
* secondary max is right, min is left
*
* @param accessor
* @returns
*/
export function getMaxSpaceAttributes(accessor: CollisionAccessors) {
const { parallelMax, parallelMin, perpendicularMax, perpendicularMin } =
accessor;
return {
primary: { max: perpendicularMax, min: perpendicularMin },
secondary: { max: parallelMax, min: parallelMin },
};
}
/**
* get a particular orientation accessor based on orientation
* @param isHorizontal
* @returns horizontal or vertical
*/
export function getOrientationAccessor(isHorizontal?: boolean) {
return isHorizontal ? "horizontal" : "vertical";
}
/**
* method to get sorted occupied spaces based on direction
*
* @param occupiedSpacesMap all the occupied spaces map on the canvas
* @param accessors collision accessors to access the space's/block's data based on direction
* @returns array, sorted occupied spaces based on direction
*/
export function getSortedOccupiedSpaces(
occupiedSpacesMap: SpaceMap,
accessors: CollisionAccessors,
) {
const sortedOccupiedSpaces = Object.values(occupiedSpacesMap);
sortedOccupiedSpaces.sort((a, b) => {
return a[accessors.direction] - b[accessors.direction];
});
return sortedOccupiedSpaces;
}
/**
* method to get sorted new spaces based on direction
*
* @param newSpacePositions new/current positions array of the space/block
* @param accessors collision accessors to access the space's/block's data based on direction
* @returns array, sorted new spaces based on direction
*/
export function getSortedNewPositions(
newSpacePositionsMap: SpaceMap,
accessors: CollisionAccessors,
) {
const newSpacePositions = Object.values(newSpacePositionsMap);
newSpacePositions
.sort((a, b) => {
return a[accessors.direction] - b[accessors.direction];
})
.map((a) => {
return { ...a, order: true };
});
return newSpacePositions;
}
/**
* method to get sorted colliding spaces based on previous collision order
*
* @param collidingSpaceMap direct collision spaces map of the current new space positions
* @param isHorizontal boolean to indicate if the orientation is horizontal
* @param primaryCollisionMap direct collision spaces map on the previous run of the algorithm
* @returns array, sorted occupied spaces based on direction
*/
export function getSortedCollidingSpaces(
collidingSpaceMap: CollisionMap,
isHorizontal: boolean,
prevCollisionMap: CollisionMap,
) {
const collidingSpaces = Object.values(collidingSpaceMap).filter(
(a) => a.isHorizontal === isHorizontal,
);
if (!collidingSpaces.length) return [];
collidingSpaces.sort((a, b) => {
const collisionKeyA = a.id,
collisionKeyB = b.id;
if (prevCollisionMap) {
if (prevCollisionMap[collisionKeyA] && prevCollisionMap[collisionKeyB]) {
return (
prevCollisionMap[collisionKeyA].order -
prevCollisionMap[collisionKeyB].order
);
} else if (prevCollisionMap[collisionKeyA]) return -1;
else if (prevCollisionMap[collisionKeyB]) return 1;
}
return a.order - b.order;
});
return collidingSpaces;
}
/**
* method to get a calculated direction based on previous space positions
*
* @param newSpacePositionsMap new/current positions map of the space/block
* @param prevSpacesMap previous space positions map of the space/block
* @param passedDirection ReflowedDirection Passed from the main method
* @returns calculated direction
*/
export function getCalculatedDirection(
newSpacePositionsMap: SpaceMap,
prevSpacesMap: SpaceMap,
passedDirection: ReflowDirection,
) {
if (passedDirection.indexOf("|") >= 0) return [passedDirection];
for (const key in newSpacePositionsMap) {
if (newSpacePositionsMap[key] && prevSpacesMap[key]) {
const { left: newLeft, top: newTop } = newSpacePositionsMap[key];
const { left: prevLeft, top: prevTop } = prevSpacesMap[key];
if (newTop !== prevTop && newLeft !== prevLeft) {
return [
compareNumbers(newTop, prevTop, true)
? ReflowDirection.BOTTOM
: ReflowDirection.TOP,
compareNumbers(newLeft, prevLeft, true)
? ReflowDirection.RIGHT
: ReflowDirection.LEFT,
];
}
if (newTop !== prevTop)
return compareNumbers(newTop, prevTop, true)
? [ReflowDirection.BOTTOM]
: [ReflowDirection.TOP];
if (newLeft !== prevLeft)
return compareNumbers(newLeft, prevLeft, true)
? [ReflowDirection.RIGHT]
: [ReflowDirection.LEFT];
return [passedDirection];
}
}
return [passedDirection];
}
/**
* Returns the bottom most row among all the widget
* @param newPositions
* @returns number, the bottom most row
*/
export function getBottomMostRow(newPositions: OccupiedSpace[]): number {
let maxBottomRow = 0;
for (const newPosition of newPositions) {
maxBottomRow = Math.max(maxBottomRow, newPosition.bottom);
}
return maxBottomRow;
}
/**
* Returns a boolean to indicate if the colliding Space is to be processed if it is already processed once
*
* @param collidingSpace
* @param globalProcessedNodes
* @returns boolean, if true will process the colliding Space while generating a Tree.
*/
export function checkProcessNodeForTree(
collidingSpace: CollidingSpace,
globalProcessedNodes: CollisionTreeCache,
) {
if (!globalProcessedNodes[collidingSpace.id])
return { shouldProcessNode: true };
const direction = collidingSpace.direction;
const oppositeDirection = getOppositeDirection(direction);
// If the current node is already processed in the opposite direction the return false
if (!isUndefined(globalProcessedNodes[collidingSpace.id][oppositeDirection]))
return { shouldProcessNode: false };
const { directionIndicator } = getAccessor(direction);
// if the current node is not processed return true or if it is processed but
// the current colliding value is greater than previous' return true
if (
isUndefined(
globalProcessedNodes[collidingSpace.id][collidingSpace.direction],
) ||
compareNumbers(
collidingSpace.collidingValue,
globalProcessedNodes[collidingSpace.id][direction].value,
directionIndicator > 0,
)
)
return { shouldProcessNode: true };
//if collision values are equal, return the cached values to be used in calculation
const {
childNode,
currentEmptySpaces,
occupiedLength,
occupiedSpace,
value,
} = globalProcessedNodes[collidingSpace.id][direction];
if (collidingSpace.collidingValue === value)
return {
shouldProcessNode: false,
currentChildNode: childNode,
occupiedLength,
occupiedSpace,
currentEmptySpaces,
};
return {
shouldProcessNode: false,
};
}
/**
* This is to get the colliding value relative to the edge of the canvas.
* eg, If a widget is colliding with another, near the ege of the canvas.
* After the point where the widget resizes full and can'yt move or resize, then the colliding value also should not increase
*
* @param accessors
* @param collidingValue
* @param direction
* @param gridProps
* @param occupiedLength
* @returns number, colliding value to the edge of canvas
*/
export function getRelativeCollidingValue(
accessors: CollisionAccessors,
collidingValue: number,
direction: ReflowDirection,
{ maxGridColumns }: GridProps,
occupiedLength?: number,
): number {
if (direction === ReflowDirection.BOTTOM || !occupiedLength)
return collidingValue;
let calculatedCollidingValue = occupiedLength;
if (direction === ReflowDirection.RIGHT)
calculatedCollidingValue = maxGridColumns - calculatedCollidingValue;
// return the maximum or minimum based on the direction
return Math[accessors.mathComparator](
calculatedCollidingValue,
collidingValue,
);
}
/**
* Get the edge from which the widget just exited
* @param exitContainer Id of the container that was just exited
* @param mousePosition position of mouse on Canvas Grid
* @returns
*/
export function getContainerExitEdge(
exitContainer: OccupiedSpace,
mousePosition: OccupiedSpace,
) {
if (
mousePosition.top > exitContainer.top &&
mousePosition.top < exitContainer.bottom
) {
if (mousePosition.left >= exitContainer.right) return ReflowDirection.RIGHT;
if (mousePosition.left <= exitContainer.left) return ReflowDirection.LEFT;
}
if (
mousePosition.left > exitContainer.left &&
mousePosition.left < exitContainer.right
) {
if (mousePosition.top >= exitContainer.bottom)
return ReflowDirection.BOTTOM;
if (mousePosition.top <= exitContainer.top) return ReflowDirection.TOP;
}
}
/**
* If we are not reflowing the drop targets,
* then we will have to figure out the direction in which it is colliding with dragging Spaces
* @param containerSpace Space positions of Container/Droptargets
* @param currentDirection current Direction
* @param mousePosition Position of mouse on Canvas Grid
* @returns
*/
export function getCollisionDirectionOfDropTarget(
containerSpace: BlockSpace,
currentDirection: ReflowDirection,
mousePosition?: OccupiedSpace,
): ReflowDirection {
const possiblePushDirections = getPossiblePushDirections(
containerSpace,
mousePosition,
);
if (
possiblePushDirections.length < 1 ||
possiblePushDirections.includes(currentDirection)
) {
return currentDirection;
}
return possiblePushDirections[0];
}
/**
* Get the possible directions the Containers/drop targets can be pushed based on mousePosition
* @param containerSpace Space positions of Container/Droptargets
* @param mousePosition Position of mouse on Canvas Grid
* @returns Array of Possible directions, at the max two directions based on mouse positions,
* sorted by distance to container of mouse position
*/
function getPossiblePushDirections(
containerSpace: BlockSpace,
mousePosition?: OccupiedSpace,
): ReflowDirection[] {
if (!mousePosition) return [];
const directionsWithDistance: {
distance: number;
direction: ReflowDirection;
}[] = [];
if (containerSpace.left >= mousePosition.left) {
directionsWithDistance.push({
distance: containerSpace.left - mousePosition.left,
direction: ReflowDirection.RIGHT,
});
} else if (mousePosition.left >= containerSpace.right) {
directionsWithDistance.push({
distance: mousePosition.left - containerSpace.right,
direction: ReflowDirection.LEFT,
});
}
if (containerSpace.top >= mousePosition.top) {
directionsWithDistance.push({
distance: containerSpace.top - mousePosition.top,
direction: ReflowDirection.BOTTOM,
});
} else if (mousePosition.top >= containerSpace.bottom) {
directionsWithDistance.push({
distance: mousePosition.top - containerSpace.bottom,
direction: ReflowDirection.TOP,
});
}
return directionsWithDistance
.sort((a, b) => {
return b.distance - a.distance;
})
.map((obj) => obj.direction);
}
/**
* Resize the dragging widget on collision with Container/Droptarget widget
* @param newSpacePositions positions of dragging spaces
* @param occupiedSpaces occupied spaces of other blocks on the canvas
* @param mousePosition position of mouse on canvas grid
* @param direction ReflowDirection
* @param orientationalAccessor "vertical" or "horizontal"
* @param prevCollidingSpaceMap previous colliding map
* @param forceDirection boolean to indicate if direction should be forced
* @param isHorizontalMove boolean
* @param prevSpacesMap previous position maps
* @returns
*/
export function resizeOnContainerCollision(
newSpacePositions: BlockSpace[],
occupiedSpaces: BlockSpace[],
mousePosition: OccupiedSpace | undefined,
direction: ReflowDirection,
orientationalAccessor: "horizontal" | "vertical",
prevCollidingSpaceMap: CollidingSpaceMap,
forceDirection: boolean,
isHorizontalMove?: boolean,
prevSpacesMap?: SpaceMap,
): {
reflowableOccSpaces: BlockSpace[];
currSpacePositions: BlockSpace[];
shouldRegisterContainerTimeout: boolean;
} {
const reflowableOccSpaces = [];
//boolean to indicate if this run should be registered for timeout
let shouldRegisterContainerTimeout = false;
// resize space positions only is single space is being dragged
if (newSpacePositions.length > 1) {
return {
reflowableOccSpaces: occupiedSpaces.filter(
(occSpace) => !occSpace.isDropTarget,
),
currSpacePositions: newSpacePositions,
shouldRegisterContainerTimeout: occupiedSpaces.some(
(space) => space.isDropTarget,
),
};
}
let currSpacePosition = { ...newSpacePositions[0] };
for (const occupiedSpace of occupiedSpaces) {
if (areOverlapping(occupiedSpace, currSpacePosition)) {
if (!occupiedSpace.isDropTarget) {
reflowableOccSpaces.push(occupiedSpace);
continue;
}
//get calculated direction
let movementDirection = getCorrectedDirection(
occupiedSpace,
prevSpacesMap && prevSpacesMap[currSpacePosition.id]
? prevSpacesMap[currSpacePosition.id]
: undefined,
direction,
false,
prevCollidingSpaceMap && prevCollidingSpaceMap[orientationalAccessor],
isHorizontalMove,
);
//check if direction could be changed because of mouse Position
movementDirection = getCollisionDirectionOfDropTarget(
occupiedSpace,
movementDirection,
mousePosition,
);
//modify/resize dragging position if required
currSpacePosition = modifyResizePosition(
currSpacePosition,
occupiedSpace,
forceDirection ? direction : movementDirection,
);
// setting it to true as it should register a timeout function
shouldRegisterContainerTimeout = true;
}
}
return {
reflowableOccSpaces,
currSpacePositions: [currSpacePosition],
shouldRegisterContainerTimeout,
};
}
/**
* Modify the Space position when colliding with
* @param newSpacePositions positions of dragging spaces
* @param collidingContainer Space positions of Container/Droptargets
* @param direction ReflowDirection
* @returns
*/
export function modifyResizePosition(
newSpacePosition: BlockSpace,
collidingContainer: BlockSpace,
direction: ReflowDirection,
): BlockSpace {
if (!direction || direction === ReflowDirection.UNSET) {
return newSpacePosition;
}
const spacePosition = { ...newSpacePosition };
const {
direction: directionAccessor,
directionIndicator,
isHorizontal,
mathComparator,
oppositeDirection,
oppositeMathComparator,
} = getAccessor(direction);
spacePosition[directionAccessor] = Math[mathComparator](
spacePosition[directionAccessor],
collidingContainer[oppositeDirection],
);
const minDimension = isHorizontal
? HORIZONTAL_RESIZE_MIN_LIMIT
: newSpacePosition.fixedHeight === undefined
? VERTICAL_RESIZE_MIN_LIMIT
: newSpacePosition.fixedHeight;
spacePosition[directionAccessor] = Math[oppositeMathComparator](
spacePosition[directionAccessor],
spacePosition[oppositeDirection] + directionIndicator * minDimension,
);
return spacePosition;
}
/**
* Checks if any of the widget has reached it's movements limit and returns true if it has
* @param movementLimitMap
* @returns boolean
*/
export function willItCauseUndroppableState(
movementLimitMap: MovementLimitMap | undefined,
) {
if (!movementLimitMap) return true;
const movementLimits = Object.values(movementLimitMap);
return movementLimits.some(
(limit) => !(limit.canHorizontalMove && limit.canVerticalMove),
);
}
/**
* verify the widget being dragged is colliding with drop targets when they are not being reflowed
* @param movementLimitMap
* @param spacePositionMap
* @param occupiedSpacesMap
* @returns
*/
export function verifyMovementLimits(
movementLimitMap: MovementLimitMap,
spacePositionMap: SpaceMap,
occupiedSpacesMap: SpaceMap,
) {
for (const spaceId in spacePositionMap) {
for (const occupiedSpaceId in occupiedSpacesMap) {
if (
occupiedSpacesMap[occupiedSpaceId].isDropTarget &&
areIntersecting(
occupiedSpacesMap[occupiedSpaceId],
spacePositionMap[spaceId],
)
) {
movementLimitMap[spaceId] = {
canHorizontalMove: false,
canVerticalMove: false,
};
}
}
}
return movementLimitMap;
}