## 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 -->
2261 lines
67 KiB
TypeScript
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;
|
|
}
|