@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.
- package/board/interfaces.d.ts +2 -0
- package/board/internal.js +4 -4
- package/board/transition.js +2 -1
- package/board/utils/layout.js +4 -8
- package/internal/debug-tools/generators.js +1 -1
- package/internal/dnd-controller/controller.d.ts +2 -2
- package/internal/dnd-controller/controller.js +4 -4
- package/internal/environment.js +1 -1
- package/internal/environment.json +1 -1
- package/internal/item-container/index.d.ts +1 -1
- package/internal/item-container/index.js +4 -3
- package/internal/layout-engine/engine-cache.d.ts +32 -0
- package/internal/layout-engine/engine-cache.js +43 -0
- package/internal/layout-engine/engine-solution.d.ts +25 -0
- package/internal/layout-engine/engine-solution.js +205 -0
- package/internal/layout-engine/engine-state.d.ts +17 -0
- package/internal/layout-engine/engine-state.js +13 -0
- package/internal/layout-engine/engine-step.d.ts +8 -10
- package/internal/layout-engine/engine-step.js +184 -348
- package/internal/layout-engine/engine.d.ts +17 -13
- package/internal/layout-engine/engine.js +67 -59
- package/internal/layout-engine/grid.d.ts +8 -19
- package/internal/layout-engine/grid.js +36 -98
- package/internal/layout-engine/interfaces.d.ts +6 -2
- package/internal/layout-engine/utils.d.ts +8 -3
- package/internal/layout-engine/utils.js +48 -9
- package/internal/manifest.json +1 -1
- package/items-palette/internal.js +4 -6
- package/package.json +1 -1
- package/internal/utils/stack-set.d.ts +0 -8
- package/internal/utils/stack-set.js +0 -23
package/board/interfaces.d.ts
CHANGED
|
@@ -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,
|
|
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:
|
|
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)
|
|
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:
|
|
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),
|
package/board/transition.js
CHANGED
|
@@ -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)
|
|
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
|
}
|
package/board/utils/layout.js
CHANGED
|
@@ -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
|
|
62
|
+
return transition.layoutEngine.resize({ itemId: transition.draggableItem.id, path });
|
|
65
63
|
case "reorder":
|
|
66
|
-
return
|
|
64
|
+
return transition.layoutEngine.move({ itemId: transition.draggableItem.id, path });
|
|
67
65
|
case "insert":
|
|
68
|
-
return
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
119
|
-
controller.acquire(droppableId,
|
|
118
|
+
acquire(droppableId, renderAcquiredItem) {
|
|
119
|
+
controller.acquire(droppableId, renderAcquiredItem);
|
|
120
120
|
},
|
|
121
121
|
getDroppables() {
|
|
122
122
|
return controller.getDroppables();
|
package/internal/environment.js
CHANGED
|
@@ -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,
|
|
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 (
|
|
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 {
|
|
2
|
-
import { LayoutEngineGrid, ReadonlyLayoutEngineGrid } from "./grid";
|
|
1
|
+
import { LayoutEngineState } from "./engine-state";
|
|
3
2
|
import { CommittedMove } from "./interfaces";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export declare function resolveOverlaps(
|
|
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;
|