@cloudscape-design/board-components 3.0.30 → 3.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { ReactNode } from "react";
2
2
  import { InteractionType, Operation } from "../internal/dnd-controller/controller";
3
3
  import { BoardItemDefinition, BoardItemDefinitionBase, DataFallbackType, Direction, GridLayout, GridLayoutItem, ItemId, Rect } from "../internal/interfaces";
4
+ import { LayoutEngine } from "../internal/layout-engine/engine";
4
5
  import { LayoutShift } from "../internal/layout-engine/interfaces";
5
6
  import { NonCancelableEventHandler } from "../internal/utils/events";
6
7
  import { Position } from "../internal/utils/position";
@@ -127,6 +128,7 @@ export interface Transition<D> {
127
128
  operation: Operation;
128
129
  interactionType: InteractionType;
129
130
  itemsLayout: GridLayout;
131
+ layoutEngine: LayoutEngine;
130
132
  insertionDirection: null | Direction;
131
133
  draggableItem: BoardItemDefinitionBase<D>;
132
134
  draggableRect: Rect;
package/board/internal.js CHANGED
@@ -132,7 +132,7 @@ export function InternalBoard({ items, renderItem, onItemsChange, empty, i18nStr
132
132
  dispatch({ type: "discard" });
133
133
  autoScrollHandlers.removePointerEventHandlers();
134
134
  });
135
- useDragSubscription("acquire", ({ droppableId, draggableItem, acquiredItemElement }) => {
135
+ useDragSubscription("acquire", ({ droppableId, draggableItem, renderAcquiredItem }) => {
136
136
  const placeholder = placeholdersLayout.items.find((it) => it.id === droppableId);
137
137
  // If missing then it does not belong to this board.
138
138
  if (!placeholder) {
@@ -142,13 +142,13 @@ export function InternalBoard({ items, renderItem, onItemsChange, empty, i18nStr
142
142
  type: "acquire-item",
143
143
  position: new Position({ x: placeholder.x, y: placeholder.y }),
144
144
  layoutElement: containerAccessRef.current,
145
- acquiredItemElement: acquiredItemElement,
145
+ acquiredItemElement: renderAcquiredItem(),
146
146
  });
147
147
  focusNextRenderIdRef.current = draggableItem.id;
148
148
  });
149
149
  const removeItemAction = (removedItem) => {
150
150
  dispatch({ type: "init-remove", items, itemsLayout, removedItem });
151
- const layoutShift = new LayoutEngine(itemsLayout).remove(removedItem.id).getLayoutShift();
151
+ const layoutShift = new LayoutEngine(itemsLayout).remove(removedItem.id);
152
152
  onItemsChange(createItemsChangeEvent(items, layoutShift));
153
153
  };
154
154
  function focusItem(itemId) {
@@ -192,7 +192,7 @@ export function InternalBoard({ items, renderItem, onItemsChange, empty, i18nStr
192
192
  else {
193
193
  delete itemContainerRef.current[item.id];
194
194
  }
195
- }, item: item, transform: transforms[item.id], inTransition: !!transition || !!removeTransition, placed: true, acquired: item.id === (acquiredItem === null || acquiredItem === void 0 ? void 0 : acquiredItem.id), getItemSize: () => ({
195
+ }, item: item, transform: transforms[item.id], inTransition: !!transition || !!removeTransition, placed: item.id !== (acquiredItem === null || acquiredItem === void 0 ? void 0 : acquiredItem.id), acquired: item.id === (acquiredItem === null || acquiredItem === void 0 ? void 0 : acquiredItem.id), getItemSize: () => ({
196
196
  width: gridContext.getWidth(itemSize.width),
197
197
  minWidth: gridContext.getWidth(getMinColumnSpan(item, itemsLayout.columns)),
198
198
  maxWidth: gridContext.getWidth(itemMaxSize.width),
@@ -38,6 +38,7 @@ function initTransition({ operation, interactionType, itemsLayout, draggableItem
38
38
  operation,
39
39
  interactionType,
40
40
  itemsLayout,
41
+ layoutEngine: new LayoutEngine(itemsLayout),
41
42
  insertionDirection: null,
42
43
  draggableItem,
43
44
  draggableRect,
@@ -67,7 +68,7 @@ function initTransition({ operation, interactionType, itemsLayout, draggableItem
67
68
  };
68
69
  }
69
70
  function initRemoveTransition({ items, removedItem, itemsLayout }) {
70
- const layoutShift = new LayoutEngine(itemsLayout).remove(removedItem.id).refloat().getLayoutShift();
71
+ const layoutShift = new LayoutEngine(itemsLayout).remove(removedItem.id);
71
72
  const removeTransition = { items, removedItem, layoutShift };
72
73
  return { transition: null, removeTransition, announcement: null };
73
74
  }
@@ -1,6 +1,5 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
- import { LayoutEngine } from "../../internal/layout-engine/engine";
4
3
  import { createPlaceholdersLayout, getDefaultColumnSpan, getDefaultRowSpan } from "../../internal/utils/layout";
5
4
  import { normalizeInsertionPath } from "./path";
6
5
  export function getLayoutColumns(transition) {
@@ -54,24 +53,21 @@ export function getLayoutShift(transition, path, insertionDirection) {
54
53
  if (path.length === 0) {
55
54
  return null;
56
55
  }
57
- const engine = new LayoutEngine(transition.itemsLayout);
58
56
  const width = getDefaultColumnSpan(transition.draggableItem, getLayoutColumns(transition));
59
57
  const height = getDefaultRowSpan(transition.draggableItem);
60
58
  const rows = getLayoutRows(transition);
61
59
  const columns = getLayoutColumns(transition);
62
60
  switch (transition.operation) {
63
61
  case "resize":
64
- return engine.resize({ itemId: transition.draggableItem.id, path }).getLayoutShift();
62
+ return transition.layoutEngine.resize({ itemId: transition.draggableItem.id, path });
65
63
  case "reorder":
66
- return engine.move({ itemId: transition.draggableItem.id, path }).getLayoutShift();
64
+ return transition.layoutEngine.move({ itemId: transition.draggableItem.id, path });
67
65
  case "insert":
68
- return engine
69
- .insert({
66
+ return transition.layoutEngine.insert({
70
67
  itemId: transition.draggableItem.id,
71
68
  width,
72
69
  height,
73
70
  path: normalizeInsertionPath(path, insertionDirection !== null && insertionDirection !== void 0 ? insertionDirection : "right", columns, rows),
74
- })
75
- .getLayoutShift();
71
+ });
76
72
  }
77
73
  }
@@ -218,7 +218,7 @@ export function generateInsert(grid, insertId = "X", options) {
218
218
  const textGrid = toMatrix(grid);
219
219
  const y = getRandomIndex(textGrid);
220
220
  const x = getRandomIndex(textGrid[y]);
221
- const width = getRandomInt(1, maxWidth + 1 - x);
221
+ const width = getRandomInt(1, Math.max(1, maxWidth + 1 - x));
222
222
  const height = getRandomInt(1, maxHeight + 1);
223
223
  return { itemId: insertId, width, height, path: [new Position({ x, y })] };
224
224
  }
@@ -36,7 +36,7 @@ export interface Droppable {
36
36
  interface AcquireData {
37
37
  droppableId: ItemId;
38
38
  draggableItem: Item;
39
- acquiredItemElement?: ReactNode;
39
+ renderAcquiredItem: () => ReactNode;
40
40
  }
41
41
  export interface DragAndDropEvents {
42
42
  start: (data: DragAndDropData) => void;
@@ -54,7 +54,7 @@ export declare function useDraggable({ draggableItem, getCollisionRect, }: {
54
54
  updateTransition(coordinates: Coordinates): void;
55
55
  submitTransition(): void;
56
56
  discardTransition(): void;
57
- acquire(droppableId: ItemId, acquiredItemElement?: ReactNode): void;
57
+ acquire(droppableId: ItemId, renderAcquiredItem: () => ReactNode): void;
58
58
  getDroppables(): [string, Droppable][];
59
59
  };
60
60
  export declare function useDroppable({ itemId, context, getElement, }: {
@@ -43,11 +43,11 @@ class DragAndDropController extends EventEmitter {
43
43
  /**
44
44
  * Issues an "acquire" event to notify the current transition draggable is acquired by the given droppable.
45
45
  */
46
- acquire(droppableId, acquiredItemElement) {
46
+ acquire(droppableId, renderAcquiredItem) {
47
47
  if (!this.transition) {
48
48
  throw new Error("Invariant violation: no transition present for acquire.");
49
49
  }
50
- this.emit("acquire", { droppableId, draggableItem: this.transition.draggableItem, acquiredItemElement });
50
+ this.emit("acquire", { droppableId, draggableItem: this.transition.draggableItem, renderAcquiredItem });
51
51
  }
52
52
  /**
53
53
  * Registers a droppable used for collisions check, acquire, and dropTarget provision.
@@ -115,8 +115,8 @@ export function useDraggable({ draggableItem, getCollisionRect, }) {
115
115
  discardTransition() {
116
116
  controller.discard();
117
117
  },
118
- acquire(droppableId, acquiredItemElement) {
119
- controller.acquire(droppableId, acquiredItemElement);
118
+ acquire(droppableId, renderAcquiredItem) {
119
+ controller.acquire(droppableId, renderAcquiredItem);
120
120
  },
121
121
  getDroppables() {
122
122
  return controller.getDroppables();
@@ -1,4 +1,4 @@
1
1
  export var PACKAGE_SOURCE = "board-components";
2
- export var PACKAGE_VERSION = "3.0.0 (5dff2947)";
2
+ export var PACKAGE_VERSION = "3.0.0 (4a7c9c80)";
3
3
  export var THEME = "open-source-visual-refresh";
4
4
  export var ALWAYS_VISUAL_REFRESH = true;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "PACKAGE_SOURCE": "board-components",
3
- "PACKAGE_VERSION": "3.0.0 (5dff2947)",
3
+ "PACKAGE_VERSION": "3.0.0 (4a7c9c80)",
4
4
  "THEME": "open-source-visual-refresh",
5
5
  "ALWAYS_VISUAL_REFRESH": true
6
6
  }
@@ -46,7 +46,7 @@ export interface ItemContainerProps {
46
46
  maxHeight: number;
47
47
  };
48
48
  onKeyMove?(direction: Direction): void;
49
- children: () => ReactNode;
49
+ children: (hasDropTarget: boolean) => ReactNode;
50
50
  }
51
51
  export declare const ItemContainer: import("react").ForwardRefExoticComponent<ItemContainerProps & import("react").RefAttributes<ItemContainerRef>>;
52
52
  export {};
@@ -60,6 +60,7 @@ function ItemContainerComponent({ item, placed, acquired, inTransition, transfor
60
60
  itemId: draggableItem.id,
61
61
  sizeTransform: dropTarget ? getItemSize(dropTarget) : originalSizeRef.current,
62
62
  positionTransform: { x: coordinates.x - pointerOffset.x, y: coordinates.y - pointerOffset.y },
63
+ hasDropTarget: !!dropTarget,
63
64
  });
64
65
  }
65
66
  }
@@ -154,7 +155,7 @@ function ItemContainerComponent({ item, placed, acquired, inTransition, transfor
154
155
  return;
155
156
  }
156
157
  // Notify the respective droppable of the intention to insert the item in it.
157
- draggableApi.acquire(nextDroppable, childrenRef.current);
158
+ draggableApi.acquire(nextDroppable, () => children(true));
158
159
  setIsHidden(true);
159
160
  muteEventsRef.current = true;
160
161
  }
@@ -244,7 +245,7 @@ function ItemContainerComponent({ item, placed, acquired, inTransition, transfor
244
245
  if (isHidden) {
245
246
  itemTransitionClassNames.push(styles.hidden);
246
247
  }
247
- if (placed && transform) {
248
+ if (transform) {
248
249
  // The moved items positions are altered with CSS transform.
249
250
  if (transform.type === "move") {
250
251
  itemTransitionClassNames.push(styles.transformed);
@@ -272,7 +273,7 @@ function ItemContainerComponent({ item, placed, acquired, inTransition, transfor
272
273
  (transition === null || transition === void 0 ? void 0 : transition.interactionType) === "pointer";
273
274
  const childrenRef = useRef(null);
274
275
  if (!inTransition || isActive) {
275
- childrenRef.current = children();
276
+ childrenRef.current = children(!!(transition === null || transition === void 0 ? void 0 : transition.hasDropTarget));
276
277
  }
277
278
  const content = (_jsx("div", { ref: itemRef, className: clsx(styles.root, ...itemTransitionClassNames), style: itemTransitionStyle, "data-item-id": item.id, onBlur: onBlur, children: _jsx(ItemContext.Provider, { value: {
278
279
  isActive,
@@ -0,0 +1,32 @@
1
+ import { Position } from "../utils/position";
2
+ import { LayoutEngineState } from "./engine-state";
3
+ /**
4
+ * The cache is used to avoid duplicate computations for the same initial state and path.
5
+ * The cache must be invalidated once the items layout has changed e.g. as result of the operation commit.
6
+ * The cache is a tree of nodes with the root node representing the initial state and empty path. The
7
+ * rest of the tree store all previous state computations per path.
8
+ */
9
+ export declare class LayoutEngineCacheNode {
10
+ position: null | Position;
11
+ state: LayoutEngineState;
12
+ private next;
13
+ constructor(state: LayoutEngineState);
14
+ /**
15
+ * The function takes path position and the callback to compute the corresponding state if not yet cached.
16
+ * It returns the next cache node to take the next path position if available:
17
+ *
18
+ * const root = new LayoutEngineCacheNode(state)
19
+ *
20
+ * const x1y0 = root
21
+ * .matches({ x: 0, y: 0 }, () => compute({ x: 0, y: 0 })) // computes
22
+ * .matches({ x: 1, y: 0 }, () => compute({ x: 1, y: 0 })) // computes
23
+ * .state;
24
+ *
25
+ * const x2y0 = root
26
+ * .matches({ x: 0, y: 0 }, () => compute({ x: 0, y: 0 }))
27
+ * .matches({ x: 1, y: 0 }, () => compute({ x: 1, y: 0 }))
28
+ * .matches({ x: 2, y: 0 }, () => compute({ x: 2, y: 0 })) // computes
29
+ * .state;
30
+ */
31
+ matches(position: Position, compute: () => LayoutEngineState): LayoutEngineCacheNode;
32
+ }
@@ -0,0 +1,43 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * The cache is used to avoid duplicate computations for the same initial state and path.
5
+ * The cache must be invalidated once the items layout has changed e.g. as result of the operation commit.
6
+ * The cache is a tree of nodes with the root node representing the initial state and empty path. The
7
+ * rest of the tree store all previous state computations per path.
8
+ */
9
+ export class LayoutEngineCacheNode {
10
+ constructor(state) {
11
+ this.position = null;
12
+ this.next = new Array();
13
+ this.state = state;
14
+ }
15
+ /**
16
+ * The function takes path position and the callback to compute the corresponding state if not yet cached.
17
+ * It returns the next cache node to take the next path position if available:
18
+ *
19
+ * const root = new LayoutEngineCacheNode(state)
20
+ *
21
+ * const x1y0 = root
22
+ * .matches({ x: 0, y: 0 }, () => compute({ x: 0, y: 0 })) // computes
23
+ * .matches({ x: 1, y: 0 }, () => compute({ x: 1, y: 0 })) // computes
24
+ * .state;
25
+ *
26
+ * const x2y0 = root
27
+ * .matches({ x: 0, y: 0 }, () => compute({ x: 0, y: 0 }))
28
+ * .matches({ x: 1, y: 0 }, () => compute({ x: 1, y: 0 }))
29
+ * .matches({ x: 2, y: 0 }, () => compute({ x: 2, y: 0 })) // computes
30
+ * .state;
31
+ */
32
+ matches(position, compute) {
33
+ for (const nextNode of this.next) {
34
+ if (nextNode.position.x === position.x && nextNode.position.y === position.y) {
35
+ return nextNode;
36
+ }
37
+ }
38
+ const nextNode = new LayoutEngineCacheNode(compute());
39
+ nextNode.position = position;
40
+ this.next.push(nextNode);
41
+ return nextNode;
42
+ }
43
+ }
@@ -0,0 +1,25 @@
1
+ import { Conflicts } from "./engine-state";
2
+ import { LayoutEngineGrid, ReadonlyLayoutEngineGrid } from "./grid";
3
+ import { CommittedMove } from "./interfaces";
4
+ export type MoveSolution = [state: MoveSolutionState, nextMove: CommittedMove];
5
+ export declare class MoveSolutionState {
6
+ grid: LayoutEngineGrid;
7
+ moves: CommittedMove[];
8
+ moveIndex: number;
9
+ conflicts: null | Conflicts;
10
+ overlaps: Map<string, string>;
11
+ score: number;
12
+ constructor(grid: ReadonlyLayoutEngineGrid, moves: readonly CommittedMove[], conflicts: null | Conflicts);
13
+ static clone({ grid, moves, moveIndex, conflicts, overlaps, score }: MoveSolutionState): {
14
+ grid: LayoutEngineGrid;
15
+ moves: CommittedMove[];
16
+ moveIndex: number;
17
+ conflicts: Conflicts | null;
18
+ overlaps: Map<string, string>;
19
+ score: number;
20
+ };
21
+ }
22
+ /**
23
+ * Given a solution state finds a set of all possible moves each resolving a particular overlap.
24
+ */
25
+ export declare function findNextSolutions(state: MoveSolutionState): MoveSolution[];
@@ -0,0 +1,205 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Position } from "../utils/position";
4
+ import { LayoutEngineGrid } from "./grid";
5
+ import { checkOppositeDirections, createMove, getMoveOriginalRect, getMoveRect } from "./utils";
6
+ // All directions in which overlaps can be incrementally resolved.
7
+ const PRIORITY_DIRECTIONS = ["down", "right", "left", "up"];
8
+ // The class represents an intermediate layout state used to find the next set of solutions for.
9
+ // The solution is terminal when no overlaps are left and it can become the next layout state if its
10
+ // score is smaller than that of the alternative solutions.
11
+ export class MoveSolutionState {
12
+ constructor(grid, moves, conflicts) {
13
+ this.moveIndex = 0;
14
+ this.overlaps = new Map();
15
+ this.score = 0;
16
+ this.grid = LayoutEngineGrid.clone(grid);
17
+ this.moves = [...moves];
18
+ this.moveIndex = moves.length;
19
+ this.conflicts = conflicts;
20
+ }
21
+ // The solution state needs to be cloned after the move is performed in case there are overlaps left
22
+ // so that the next solutions won't have the shared state to corrupt.
23
+ // The conflicts never change and can be carried over w/o cloning.
24
+ static clone({ grid, moves, moveIndex, conflicts, overlaps, score }) {
25
+ return {
26
+ grid: LayoutEngineGrid.clone(grid),
27
+ moves: [...moves],
28
+ moveIndex,
29
+ conflicts,
30
+ overlaps: new Map([...overlaps]),
31
+ score,
32
+ };
33
+ }
34
+ }
35
+ /**
36
+ * Given a solution state finds a set of all possible moves each resolving a particular overlap.
37
+ */
38
+ export function findNextSolutions(state) {
39
+ // For every overlap and direction found a move if exists that resolves the overlap.
40
+ // A pair of the given state and the overlap resolution move is a new solution to try.
41
+ const nextMoveSolutions = [];
42
+ for (const [overlapId, overlapIssuerId] of state.overlaps) {
43
+ for (const moveDirection of PRIORITY_DIRECTIONS) {
44
+ const move = getOverlapMove(state, overlapId, overlapIssuerId, moveDirection);
45
+ if (move !== null) {
46
+ nextMoveSolutions.push([MoveSolutionState.clone(state), move]);
47
+ }
48
+ }
49
+ }
50
+ return nextMoveSolutions;
51
+ }
52
+ // Returns an evaluated move to resolve the given overlap in the given direction or null if such move is not possible.
53
+ function getOverlapMove(state, overlapId, overlapIssuerId, moveDirection) {
54
+ var _a;
55
+ const userItem = state.grid.getItem(state.moves[0].itemId);
56
+ const overlapItem = state.grid.getItem(overlapId);
57
+ const overlapIssuerItem = state.grid.getItem(overlapIssuerId);
58
+ const overlapMove = getMoveForDirection(overlapItem, overlapIssuerItem, moveDirection);
59
+ // The move position is outside the grid boundaries.
60
+ if (overlapMove.x < 0 || overlapMove.y < 0 || overlapMove.x + overlapMove.width > state.grid.width) {
61
+ return null;
62
+ }
63
+ // Subsequent item overlap moves in the opposite directions do not contribute to solution.
64
+ const prevOverlapMove = getLastSolutionMove(state, overlapItem.id);
65
+ if (prevOverlapMove && checkOppositeDirections(prevOverlapMove.direction, moveDirection)) {
66
+ return null;
67
+ }
68
+ const pathOverlaps = getPathOverlaps(state, overlapMove, overlapIssuerItem);
69
+ for (const overlap of pathOverlaps) {
70
+ // Not allowed to intersect with the user-controlled item.
71
+ if (overlap.id === userItem.id) {
72
+ return null;
73
+ }
74
+ // Not allowed to intersect with conflicting items.
75
+ if ((_a = state.conflicts) === null || _a === void 0 ? void 0 : _a.items.has(overlap.id)) {
76
+ return null;
77
+ }
78
+ // Intersecting with items having unresolved overlaps does not contribute to solution.
79
+ if (state.overlaps.has(overlap.id)) {
80
+ return null;
81
+ }
82
+ }
83
+ const lastIssuerMove = getLastSolutionMove(state, overlapIssuerItem.id);
84
+ if (!lastIssuerMove) {
85
+ throw new Error("Invariant violation: overlap issuer has no associated moves.");
86
+ }
87
+ const issuerDirection = lastIssuerMove.direction;
88
+ const isSwap = checkIfSwap(overlapMove, lastIssuerMove);
89
+ const isDifferentIssuerDirection = moveDirection !== issuerDirection;
90
+ const isOppositeIssuerDirection = checkOppositeDirections(moveDirection, issuerDirection);
91
+ const userMoveBoundaries = getUserMoveBoundaries(state);
92
+ const moveVector = getSolutionMovesVector(state);
93
+ // Swap score penalizes non-swap overlap resolutions in case the direction does not match that of the issuer.
94
+ const swapPenalty = isSwap ? 0 : 20;
95
+ const differentDirectionPenalty = !isSwap && isDifferentIssuerDirection ? 10 : 0;
96
+ const oppositeDirectionPenalty = !isSwap && isOppositeIssuerDirection ? 500 : 0;
97
+ const swapScore = swapPenalty + differentDirectionPenalty + oppositeDirectionPenalty;
98
+ // Overlaps score penalizes moves that cause additional overlaps.
99
+ const overlapsScore = pathOverlaps.size * 50;
100
+ // Boundaries score penalize movements of items that are outside the area covered by the user move.
101
+ const moveOutsideUserTopPenalty = overlapItem.y + overlapItem.height - 1 < userMoveBoundaries.top ? 500 : 0;
102
+ const moveOutsideUserLeftPenalty = overlapItem.x + overlapItem.width - 1 < userMoveBoundaries.left ? 50 : 0;
103
+ const moveOutsideUserRightPenalty = overlapItem.x > userMoveBoundaries.right ? 50 : 0;
104
+ const boundariesScore = moveOutsideUserTopPenalty + moveOutsideUserLeftPenalty + moveOutsideUserRightPenalty;
105
+ // Move vector score penalize movements that are against the common move direction of other items.
106
+ const vectorXPenalty = overlapMove.distanceX * moveVector.x < 0 ? moveVector.x * 2 : 0;
107
+ const vectorYPenalty = overlapMove.distanceY * moveVector.y < 0 ? moveVector.y * 2 : 0;
108
+ const moveVectorScore = vectorXPenalty + vectorYPenalty;
109
+ // Score starts from 1 to avoid overlap moves having 0 score which breaks the solutions cache.
110
+ const score = 1 + swapScore + overlapsScore + moveVectorScore + boundariesScore;
111
+ return { ...overlapMove, score };
112
+ }
113
+ // Retrieves the first possible move for the given direction to resolve the overlap.
114
+ function getMoveForDirection(moveTarget, overlap, direction) {
115
+ switch (direction) {
116
+ case "up":
117
+ return createMove("OVERLAP", moveTarget, new Position({ x: moveTarget.x, y: overlap.y - moveTarget.height }));
118
+ case "down":
119
+ return createMove("OVERLAP", moveTarget, new Position({ x: moveTarget.x, y: overlap.y + overlap.height }));
120
+ case "left":
121
+ return createMove("OVERLAP", moveTarget, new Position({ x: overlap.x - moveTarget.width, y: moveTarget.y }));
122
+ case "right":
123
+ return createMove("OVERLAP", moveTarget, new Position({ x: overlap.x + overlap.width, y: moveTarget.y }));
124
+ }
125
+ }
126
+ // Retrieves the last move if exists within the given solution.
127
+ function getLastSolutionMove(state, itemId) {
128
+ let lastMove = null;
129
+ for (let i = state.moves.length - 1; i >= state.moveIndex; i--) {
130
+ if (state.moves[i].itemId === itemId) {
131
+ lastMove = state.moves[i];
132
+ break;
133
+ }
134
+ }
135
+ return lastMove;
136
+ }
137
+ // Calculates vector as the amount of cell movements to either direction.
138
+ // All moves in one direction are summarized, the opposite moves cancel each other.
139
+ // The vector show in which direction (left / right, up / down) the most overlaps were resolved.
140
+ function getSolutionMovesVector(state) {
141
+ const vector = { x: 0, y: 0 };
142
+ for (let i = state.moveIndex; i < state.moves.length; i++) {
143
+ const move = state.moves[i];
144
+ if (move.type === "OVERLAP") {
145
+ vector.x += move.distanceX * move.height;
146
+ vector.y += move.distanceY * move.width;
147
+ }
148
+ }
149
+ return vector;
150
+ }
151
+ // Finds a rectangle within which the user-controlled item was moved (previous and current positions only).
152
+ // The layout items outside the boundaries are not expected to be disturbed.
153
+ function getUserMoveBoundaries(state) {
154
+ const firstUserMove = state.moves[0];
155
+ const lastUserMove = state.moves[state.moveIndex];
156
+ if (!firstUserMove || !lastUserMove || firstUserMove.itemId !== lastUserMove.itemId) {
157
+ throw new Error("Invariant violation: unexpected user move.");
158
+ }
159
+ const original = getMoveOriginalRect(lastUserMove);
160
+ const current = getMoveRect(lastUserMove);
161
+ return {
162
+ top: Math.min(original.top, current.top),
163
+ right: Math.max(original.right, current.right),
164
+ bottom: Math.max(original.bottom, current.bottom),
165
+ left: Math.min(original.left, current.left),
166
+ };
167
+ }
168
+ // Finds all overlaps that the move will cause along its path not considering the original location and original overlap.
169
+ function getPathOverlaps(state, move, overlapIssuerItem) {
170
+ const { left, right, top, bottom } = getMoveOriginalRect(move);
171
+ const startX = move.distanceX <= 0 ? move.x : right + 1;
172
+ const endX = move.distanceX < 0 ? left - 1 : right + move.distanceX;
173
+ const startY = move.distanceY <= 0 ? move.y : bottom + 1;
174
+ const endY = move.distanceY < 0 ? top - 1 : bottom + move.distanceY;
175
+ const pathOverlaps = new Set(state.grid.getOverlaps({
176
+ id: move.itemId,
177
+ x: startX,
178
+ width: 1 + endX - startX,
179
+ y: startY,
180
+ height: 1 + endY - startY,
181
+ }));
182
+ pathOverlaps.delete(overlapIssuerItem);
183
+ return pathOverlaps;
184
+ }
185
+ // Checks if the overlap move is a swap with the user-moved item.
186
+ function checkIfSwap(overlapMove, lastIssuerMove) {
187
+ if (lastIssuerMove.type !== "MOVE") {
188
+ return false;
189
+ }
190
+ if (!checkOppositeDirections(overlapMove.direction, lastIssuerMove.direction)) {
191
+ return false;
192
+ }
193
+ const overlapRect = getMoveOriginalRect(overlapMove);
194
+ const issuerRect = getMoveRect(lastIssuerMove);
195
+ switch (lastIssuerMove.direction) {
196
+ case "up":
197
+ return overlapRect.top === issuerRect.top;
198
+ case "right":
199
+ return overlapRect.right === issuerRect.right;
200
+ case "down":
201
+ return overlapRect.bottom === issuerRect.bottom;
202
+ case "left":
203
+ return overlapRect.left === issuerRect.left;
204
+ }
205
+ }
@@ -0,0 +1,17 @@
1
+ import { Direction, ItemId } from "../interfaces";
2
+ import { LayoutEngineGrid, ReadonlyLayoutEngineGrid } from "./grid";
3
+ import { CommittedMove } from "./interfaces";
4
+ /**
5
+ * The class describes the layout engine state at a particular path step.
6
+ * The state of the last performed step is the command result.
7
+ */
8
+ export declare class LayoutEngineState {
9
+ grid: ReadonlyLayoutEngineGrid;
10
+ moves: readonly CommittedMove[];
11
+ conflicts: null | Conflicts;
12
+ constructor(grid: LayoutEngineGrid, moves?: CommittedMove[], conflicts?: null | Conflicts);
13
+ }
14
+ export interface Conflicts {
15
+ items: ReadonlySet<ItemId>;
16
+ direction: Direction;
17
+ }
@@ -0,0 +1,13 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * The class describes the layout engine state at a particular path step.
5
+ * The state of the last performed step is the command result.
6
+ */
7
+ export class LayoutEngineState {
8
+ constructor(grid, moves = new Array(), conflicts = null) {
9
+ this.grid = grid;
10
+ this.moves = moves;
11
+ this.conflicts = conflicts;
12
+ }
13
+ }
@@ -1,11 +1,9 @@
1
- import { ItemId } from "../interfaces";
2
- import { LayoutEngineGrid, ReadonlyLayoutEngineGrid } from "./grid";
1
+ import { LayoutEngineState } from "./engine-state";
3
2
  import { CommittedMove } from "./interfaces";
4
- export declare class LayoutEngineStepState {
5
- grid: ReadonlyLayoutEngineGrid;
6
- moves: readonly CommittedMove[];
7
- conflicts: ReadonlySet<ItemId>;
8
- constructor(grid: LayoutEngineGrid, moves?: CommittedMove[], conflicts?: Set<string>);
9
- }
10
- export declare function resolveOverlaps(userMove: CommittedMove, state: LayoutEngineStepState): LayoutEngineStepState;
11
- export declare function refloatGrid(state: LayoutEngineStepState): LayoutEngineStepState;
3
+ /**
4
+ * The function takes the current layout state (item placements from the previous steps and all moves done so far)
5
+ * and a user command increment that describes an item transition by one cell in some direction.
6
+ * The function finds overlapping elements and resolves all overlaps if possible (always possible when no conflicts).
7
+ * The result in an updated state (new item placements, additional moves, and item conflicts if any).
8
+ */
9
+ export declare function resolveOverlaps(layoutState: LayoutEngineState, userMove: CommittedMove): LayoutEngineState;