@cloudscape-design/board-components 3.0.31 → 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 +1 -1
- package/board/transition.js +2 -1
- package/board/utils/layout.js +4 -8
- package/internal/debug-tools/generators.js +1 -1
- package/internal/environment.js +1 -1
- package/internal/environment.json +1 -1
- 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/package.json +1 -1
- package/internal/utils/stack-set.d.ts +0 -8
- package/internal/utils/stack-set.js +0 -23
|
@@ -1,376 +1,212 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
3
|
+
import { Position } from "../utils/position";
|
|
4
|
+
import { MoveSolutionState, findNextSolutions } from "./engine-solution";
|
|
5
|
+
import { checkItemsIntersection, sortGridItems } from "./utils";
|
|
6
|
+
import { createMove } from "./utils";
|
|
7
|
+
// The solutions can't be searched for infinitely in case the algorithm can't converge.
|
|
8
|
+
// The safety counter ensures there is going to be user feedback within reasonable time.
|
|
9
|
+
const MAX_SOLUTION_DEPTH = 100;
|
|
10
|
+
// At any given step only a few best solutions are taken to ensure faster convergence.
|
|
11
|
+
// The larger the number the better chance the most optimal solution is found for the given priorities
|
|
12
|
+
// at a cost of more computations made.
|
|
13
|
+
const NUM_BEST_SOLUTIONS = 5;
|
|
14
|
+
/**
|
|
15
|
+
* The function takes the current layout state (item placements from the previous steps and all moves done so far)
|
|
16
|
+
* and a user command increment that describes an item transition by one cell in some direction.
|
|
17
|
+
* The function finds overlapping elements and resolves all overlaps if possible (always possible when no conflicts).
|
|
18
|
+
* The result in an updated state (new item placements, additional moves, and item conflicts if any).
|
|
19
|
+
*/
|
|
20
|
+
export function resolveOverlaps(layoutState, userMove) {
|
|
21
|
+
// For better UX the layout engine is optimized for item swaps.
|
|
22
|
+
// The swapping is only preferred for the user-controlled item and it can only happen when the item overlaps another
|
|
23
|
+
// item past its midpoint. When the overlap is not enough, the underlying item is considered a conflict and it is not
|
|
24
|
+
// allowed to move anywhere. The user command cannot be committed at this step.
|
|
25
|
+
const conflicts = findConflicts(layoutState.grid, layoutState.conflicts, userMove);
|
|
26
|
+
// The user moves are always applied as is. When the user-controlled item overlaps with other items and there is
|
|
27
|
+
// no conflict, the type="OVERLAP" moves are performed to settle the grid so that no items overlap with one another.
|
|
28
|
+
// For this type of move multiple solutions are often available. To ensure the best result all solutions are tried
|
|
29
|
+
// and a score is given to each. The solution with the minimal score wins.
|
|
30
|
+
// The process stars from the initial state and the user move. The initial score and the user move score are 0.
|
|
31
|
+
const initialState = new MoveSolutionState(layoutState.grid, layoutState.moves, conflicts);
|
|
32
|
+
const initialSolution = [initialState, userMove];
|
|
33
|
+
// All solutions are guaranteed to have unique move sequences but different move sequences can produce the same result.
|
|
34
|
+
// As it is never expected for one item to be moved over to the same location twice the combination of the item ID,
|
|
35
|
+
// item position, and solution score can uniquely represent the solution.
|
|
36
|
+
// For earlier moves taking a solution from the cache can prevent hundreds of subsequent computations.
|
|
37
|
+
const solutionsCache = new Map();
|
|
38
|
+
const createCacheKey = ([state, move]) => `${move.itemId} ${move.x}:${move.y}:${state.score + move.score}`;
|
|
39
|
+
let moveSolutions = [initialSolution];
|
|
40
|
+
let bestSolution = null;
|
|
41
|
+
let convergenceCounter = MAX_SOLUTION_DEPTH;
|
|
42
|
+
// The resolution process continues until there is at least one reasonable solution left.
|
|
43
|
+
// The repetitive, dead-end, and expensive (compared to the best so far) solutions are excluded
|
|
44
|
+
// so that eventually no more variants to try remain.
|
|
45
|
+
// The convergence safety counter ensures the logical errors to not cause an infinite loop.
|
|
46
|
+
while (moveSolutions.length > 0) {
|
|
47
|
+
let nextSolutions = [];
|
|
48
|
+
for (let solutionIndex = 0; solutionIndex < Math.min(NUM_BEST_SOLUTIONS, moveSolutions.length); solutionIndex++) {
|
|
49
|
+
const [solutionState, solutionMove] = moveSolutions[solutionIndex];
|
|
50
|
+
// Discard the solution before performing the move if its next score is already above the best score found so far.
|
|
51
|
+
if (bestSolution && solutionState.score + solutionMove.score >= bestSolution.score) {
|
|
52
|
+
continue;
|
|
39
53
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
// Perform the move by mutating the solution's state: grid, moves, score, etc.
|
|
55
|
+
makeMove(solutionState, solutionMove);
|
|
56
|
+
// If no overlaps are left the solution is considered valid and the best so far.
|
|
57
|
+
// The next solutions having the same or higher score will be discarded.
|
|
58
|
+
if (solutionState.overlaps.size === 0) {
|
|
59
|
+
bestSolution = solutionState;
|
|
60
|
+
}
|
|
61
|
+
// Otherwise, the next set of solutions will be considered. There can be up to four solutions per overlap
|
|
62
|
+
// (by the number of possible directions to move).
|
|
63
|
+
else {
|
|
64
|
+
for (const nextSolution of findNextSolutions(solutionState)) {
|
|
65
|
+
const solutionKey = createCacheKey(nextSolution);
|
|
66
|
+
const cachedSolution = solutionsCache.get(solutionKey);
|
|
67
|
+
if (!cachedSolution) {
|
|
68
|
+
nextSolutions.push(nextSolution);
|
|
69
|
+
solutionsCache.set(solutionKey, nextSolution);
|
|
70
|
+
}
|
|
53
71
|
}
|
|
54
|
-
overlap = overlaps.pop();
|
|
55
72
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
}
|
|
74
|
+
// The solutions are ordered by the total score so that the best (so far) solutions are considered first.
|
|
75
|
+
moveSolutions = nextSolutions.sort((s1, s2) => s1[0].score + s1[1].score - (s2[0].score + s2[1].score));
|
|
76
|
+
nextSolutions = [];
|
|
77
|
+
// Reaching the convergence counter might indicate an issue with the algorithm as ideally it should converge faster.
|
|
78
|
+
// However, that does not necessarily mean the logical problem and no exception should be thrown.
|
|
79
|
+
// Instead, the current best solution if available applies or a simple solution is offered instead.
|
|
80
|
+
convergenceCounter--;
|
|
81
|
+
if (convergenceCounter <= 0) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// If there are conflicts it might not be possible to find a solution as the items are not allowed to
|
|
86
|
+
// overlap with the conflicts. In that case the initial state (with the user move applied) is returned.
|
|
87
|
+
// The user can move the item further to resolve the conflicts which will also unblock the overlaps resolution.
|
|
88
|
+
// Also, the solution might not be found due to the engine constraints. For example, the convergence number might
|
|
89
|
+
// be reached before any solution is found or the number of best solutions constraint can filter the only possible
|
|
90
|
+
// solutions away. In that case the simple solution is returned with all overlapping items pushed to the bottom.
|
|
91
|
+
if (!bestSolution) {
|
|
92
|
+
bestSolution = initialState.conflicts ? initialState : resolveOverlapsDown(initialState);
|
|
93
|
+
}
|
|
94
|
+
// After each step unless there are conflicts the type="FLOAT" moves are performed on all items
|
|
95
|
+
// but the user controlled one that can be moved to the top without overlapping with other items.
|
|
96
|
+
return bestSolution.conflicts ? bestSolution : refloatGrid(bestSolution, userMove);
|
|
97
|
+
}
|
|
98
|
+
// Resolves overlaps the simple way by pushing all overlapping items to the bottom until none is left.
|
|
99
|
+
function resolveOverlapsDown(state) {
|
|
100
|
+
// Move overlapping items to the bottom until resolved. Repeat until no overlaps left.
|
|
101
|
+
// This solution always converges because there is always free space at the bottom by design.
|
|
102
|
+
while (state.overlaps.size > 0) {
|
|
103
|
+
const overlaps = sortGridItems([...state.overlaps].map(([overlapId]) => state.grid.getItem(overlapId)));
|
|
104
|
+
for (const overlap of overlaps) {
|
|
105
|
+
let y = overlap.y + 1;
|
|
106
|
+
while (state.grid.getOverlaps({ ...overlap, y }).length > 0) {
|
|
107
|
+
y++;
|
|
67
108
|
}
|
|
68
|
-
|
|
69
|
-
tryVacantMoves();
|
|
70
|
-
this.refloatGrid(activeId);
|
|
71
|
-
return this;
|
|
72
|
-
}
|
|
73
|
-
// Find items that can "float" to the top and apply the necessary moves.
|
|
74
|
-
refloatGrid(activeId) {
|
|
75
|
-
if (this.conflicts.size > 0) {
|
|
76
|
-
return this;
|
|
109
|
+
makeMove(state, createMove("OVERLAP", overlap, new Position({ x: overlap.x, y })));
|
|
77
110
|
}
|
|
111
|
+
}
|
|
112
|
+
return state;
|
|
113
|
+
}
|
|
114
|
+
// Find items that can "float" to the top and apply the necessary moves.
|
|
115
|
+
function refloatGrid(layoutState, userMove) {
|
|
116
|
+
const state = new MoveSolutionState(layoutState.grid, layoutState.moves, layoutState.conflicts);
|
|
117
|
+
function makeRefloat() {
|
|
78
118
|
let needAnotherRefloat = false;
|
|
79
|
-
for (const item of
|
|
119
|
+
for (const item of state.grid.items) {
|
|
80
120
|
// The active item is skipped until the operation is committed.
|
|
81
|
-
if (item.id ===
|
|
121
|
+
if (item.id === (userMove === null || userMove === void 0 ? void 0 : userMove.itemId)) {
|
|
82
122
|
continue;
|
|
83
123
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
height: item.height,
|
|
90
|
-
type: "FLOAT",
|
|
91
|
-
};
|
|
92
|
-
for (move.y; move.y >= 0; move.y--) {
|
|
93
|
-
if (!this.validateVacantMove({ ...move, y: move.y - 1 }, activeId !== null && activeId !== void 0 ? activeId : "", false)) {
|
|
124
|
+
let y = item.y - 1;
|
|
125
|
+
let move = null;
|
|
126
|
+
while (y >= 0) {
|
|
127
|
+
const moveAttempt = createMove("FLOAT", item, new Position({ x: item.x, y }));
|
|
128
|
+
if (state.grid.getOverlaps({ id: item.id, ...moveAttempt }).length > 0) {
|
|
94
129
|
break;
|
|
95
130
|
}
|
|
131
|
+
y--;
|
|
132
|
+
move = moveAttempt;
|
|
96
133
|
}
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
this.moves.push(move);
|
|
134
|
+
if (move) {
|
|
135
|
+
makeMove(state, move);
|
|
100
136
|
needAnotherRefloat = true;
|
|
101
137
|
}
|
|
102
138
|
}
|
|
103
139
|
if (needAnotherRefloat) {
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
return this;
|
|
107
|
-
}
|
|
108
|
-
makeMove(move, addOverlap, priorities) {
|
|
109
|
-
switch (move.type) {
|
|
110
|
-
case "ESCAPE":
|
|
111
|
-
case "FLOAT":
|
|
112
|
-
case "MOVE":
|
|
113
|
-
case "VACANT":
|
|
114
|
-
case "PRIORITY":
|
|
115
|
-
this.grid.move(move.itemId, move.x, move.y, addOverlap);
|
|
116
|
-
break;
|
|
117
|
-
case "INSERT":
|
|
118
|
-
this.grid.insert({ id: move.itemId, ...move }, addOverlap);
|
|
119
|
-
break;
|
|
120
|
-
case "REMOVE":
|
|
121
|
-
this.grid.remove(move.itemId);
|
|
122
|
-
break;
|
|
123
|
-
case "RESIZE":
|
|
124
|
-
this.grid.resize(move.itemId, move.width, move.height, addOverlap);
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
this.moves.push(move);
|
|
128
|
-
priorities.set(move.itemId, this.getMovePriority(move));
|
|
129
|
-
}
|
|
130
|
-
getMovePriority(move) {
|
|
131
|
-
switch (move.type) {
|
|
132
|
-
case "FLOAT":
|
|
133
|
-
return 0;
|
|
134
|
-
case "VACANT":
|
|
135
|
-
return 1;
|
|
136
|
-
case "PRIORITY":
|
|
137
|
-
case "ESCAPE":
|
|
138
|
-
return 5;
|
|
139
|
-
default:
|
|
140
|
-
return 9999;
|
|
140
|
+
makeRefloat();
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
? [firstVertical, firstHorizontal, nextHorizontal, nextVertical]
|
|
152
|
-
: [firstHorizontal, firstVertical, nextVertical, nextHorizontal];
|
|
153
|
-
}
|
|
154
|
-
getResizeDirections(issuer) {
|
|
155
|
-
const diff = this.getLastStepDiff(issuer);
|
|
156
|
-
const firstVertical = diff.y > 0 ? "up" : "down";
|
|
157
|
-
const nextVertical = firstVertical === "down" ? "up" : "down";
|
|
158
|
-
const firstHorizontal = diff.x > 0 ? "left" : "right";
|
|
159
|
-
const nextHorizontal = firstHorizontal === "right" ? "left" : "right";
|
|
160
|
-
return Math.abs(diff.y) > Math.abs(diff.x)
|
|
161
|
-
? [firstVertical, firstHorizontal, nextHorizontal, nextVertical]
|
|
162
|
-
: [firstHorizontal, firstVertical, nextVertical, nextHorizontal];
|
|
163
|
-
}
|
|
164
|
-
getLastStepDiff(issuer) {
|
|
165
|
-
var _a, _b;
|
|
166
|
-
const issuerMoves = this.moves.filter((move) => move.itemId === issuer.id);
|
|
167
|
-
const originalParams = {
|
|
168
|
-
x: issuer.originalX,
|
|
169
|
-
y: issuer.originalY,
|
|
170
|
-
width: issuer.originalWidth,
|
|
171
|
-
height: issuer.originalHeight,
|
|
172
|
-
};
|
|
173
|
-
const prevIssuerMove = (_a = issuerMoves[issuerMoves.length - 2]) !== null && _a !== void 0 ? _a : originalParams;
|
|
174
|
-
const lastIssuerMove = (_b = issuerMoves[issuerMoves.length - 1]) !== null && _b !== void 0 ? _b : originalParams;
|
|
175
|
-
const diff = {
|
|
176
|
-
x: prevIssuerMove.x - lastIssuerMove.x,
|
|
177
|
-
y: prevIssuerMove.y - lastIssuerMove.y,
|
|
178
|
-
width: prevIssuerMove.width - lastIssuerMove.width,
|
|
179
|
-
height: prevIssuerMove.height - lastIssuerMove.height,
|
|
180
|
-
};
|
|
181
|
-
return diff.x || diff.y ? { x: diff.x, y: diff.y } : { x: diff.width, y: diff.height };
|
|
182
|
-
}
|
|
183
|
-
// Try finding a move that resolves an overlap by moving an item to a vacant space.
|
|
184
|
-
tryFindVacantMove(overlap, activeId, isResize) {
|
|
185
|
-
const overlapItem = this.grid.getItem(overlap);
|
|
186
|
-
const overlapWith = this.getOverlapWith(overlapItem);
|
|
187
|
-
const directions = isResize ? this.getResizeDirections(overlapWith) : this.getMoveDirections(overlapWith);
|
|
188
|
-
for (const direction of directions) {
|
|
189
|
-
const move = this.getMoveForDirection(overlapItem, overlapWith, direction, "VACANT");
|
|
190
|
-
if (this.validateVacantMove(move, activeId, isResize)) {
|
|
191
|
-
return move;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
143
|
+
makeRefloat();
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
// Finds items that cannot be resolved at the current step as of being partially overlapped by the user-move item.
|
|
147
|
+
function findConflicts(grid, previousConflicts, userMove) {
|
|
148
|
+
var _a;
|
|
149
|
+
// The conflicts are only defined for MOVE command type to make swaps possible.
|
|
150
|
+
if (userMove.type !== "MOVE") {
|
|
194
151
|
return null;
|
|
195
152
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (newY < 0 || newX < 0 || newX >= this.grid.width) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
// The probed destination is occupied.
|
|
207
|
-
if (this.grid.getCellOverlap(newX, newY, move.itemId)) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (isResize && activeId && !this.validateResizeMove(move, activeId)) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
// Find a move that resolves an overlap by moving an item over another item that has not been disturbed yet.
|
|
218
|
-
findPriorityMove(overlap, priorities, activeId, isResize) {
|
|
219
|
-
const overlapItem = this.grid.getItem(overlap);
|
|
220
|
-
const overlapWith = this.getOverlapWith(overlapItem);
|
|
221
|
-
const directions = isResize ? this.getResizeDirections(overlapWith) : this.getMoveDirections(overlapWith);
|
|
222
|
-
for (const direction of directions) {
|
|
223
|
-
const move = this.getMoveForDirection(overlapItem, overlapWith, direction, "PRIORITY");
|
|
224
|
-
if (this.validatePriorityMove(move, priorities, activeId, isResize)) {
|
|
225
|
-
return move;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// If can't find a good move - "escape" item to the bottom.
|
|
229
|
-
const move = {
|
|
230
|
-
itemId: overlapItem.id,
|
|
231
|
-
y: overlapItem.y + 1,
|
|
232
|
-
x: overlapItem.x,
|
|
233
|
-
width: overlapItem.width,
|
|
234
|
-
height: overlapItem.height,
|
|
235
|
-
type: "ESCAPE",
|
|
236
|
-
};
|
|
237
|
-
// If can't find the escape move after 999 steps down it is likely a bug in the validation.
|
|
238
|
-
for (move.y; move.y < 999; move.y++) {
|
|
239
|
-
if (this.validatePriorityMove(move, priorities, activeId, false)) {
|
|
240
|
-
return move;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
throw new Error("Invariant violation: can't find escape move.");
|
|
244
|
-
}
|
|
245
|
-
validatePriorityMove(move, priorities, activeId, isResize) {
|
|
246
|
-
var _a;
|
|
247
|
-
const moveTarget = this.grid.getItem(move.itemId);
|
|
248
|
-
for (let y = moveTarget.y; y < moveTarget.y + moveTarget.height; y++) {
|
|
249
|
-
for (let x = moveTarget.x; x < moveTarget.x + moveTarget.width; x++) {
|
|
250
|
-
const newY = move.y + (y - moveTarget.y);
|
|
251
|
-
const newX = move.x + (x - moveTarget.x);
|
|
252
|
-
// Outside the grid.
|
|
253
|
-
if (newY < 0 || newX < 0 || newX >= this.grid.width) {
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
for (const item of this.grid.getCell(newX, newY)) {
|
|
257
|
-
// Can't overlap with the active item.
|
|
258
|
-
if (item.id === activeId) {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
// The overlapping item has same or bigger priority.
|
|
262
|
-
if (((_a = priorities.get(item.id)) !== null && _a !== void 0 ? _a : 0) >= this.getMovePriority(move)) {
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
// The probed destination is currently blocked.
|
|
266
|
-
if (this.conflicts.has(item.id)) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (isResize && activeId && !this.validateResizeMove(move, activeId)) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
validateResizeMove(move, activeId) {
|
|
278
|
-
const resizeTarget = this.grid.getItem(activeId);
|
|
279
|
-
const moveTarget = this.grid.getItem(move.itemId);
|
|
280
|
-
const diff = this.getLastStepDiff(resizeTarget);
|
|
281
|
-
const direction = diff.x ? "horizontal" : "vertical";
|
|
282
|
-
const originalPlacement = {
|
|
283
|
-
isNext: resizeTarget.x + resizeTarget.originalWidth - 1 < moveTarget.x,
|
|
284
|
-
isBelow: resizeTarget.y + resizeTarget.originalHeight - 1 < moveTarget.y,
|
|
285
|
-
};
|
|
286
|
-
const nextPlacement = {
|
|
287
|
-
isNext: resizeTarget.x + resizeTarget.width - 1 < move.x,
|
|
288
|
-
isBelow: resizeTarget.y + resizeTarget.height - 1 < move.y,
|
|
289
|
-
};
|
|
290
|
-
return ((direction === "horizontal" && originalPlacement.isNext === nextPlacement.isNext) ||
|
|
291
|
-
(direction === "vertical" && originalPlacement.isBelow === nextPlacement.isBelow));
|
|
292
|
-
}
|
|
293
|
-
getOverlapWith(targetItem) {
|
|
294
|
-
for (let y = targetItem.y; y < targetItem.y + targetItem.height; y++) {
|
|
295
|
-
for (let x = targetItem.x; x < targetItem.x + targetItem.width; x++) {
|
|
296
|
-
const overlap = this.grid.getCellOverlap(x, y, targetItem.id);
|
|
297
|
-
if (overlap) {
|
|
298
|
-
return overlap;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
throw new Error("Invariant violation - no overlaps found.");
|
|
303
|
-
}
|
|
304
|
-
// Retrieve first possible move for the given direction to resolve the overlap.
|
|
305
|
-
getMoveForDirection(moveTarget, overlap, direction, moveType) {
|
|
306
|
-
const common = { itemId: moveTarget.id, width: moveTarget.width, height: moveTarget.height, type: moveType };
|
|
153
|
+
// Using existing conflict direction if available so that conflicting items would swap consistently.
|
|
154
|
+
// If only the current direction is considered the multi-item conflicts become difficult to comprehend.
|
|
155
|
+
const direction = (_a = previousConflicts === null || previousConflicts === void 0 ? void 0 : previousConflicts.direction) !== null && _a !== void 0 ? _a : userMove.direction;
|
|
156
|
+
// Conflicts are partial overlaps. When the item is overlapped fully (considering the direction) it is
|
|
157
|
+
// no longer treated as conflict.
|
|
158
|
+
const overlaps = grid.getOverlaps({ ...userMove, id: userMove.itemId });
|
|
159
|
+
const conflicts = overlaps.filter((overlap) => {
|
|
307
160
|
switch (direction) {
|
|
308
|
-
case "
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
case "
|
|
315
|
-
return
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
161
|
+
case "left":
|
|
162
|
+
return overlap.x < userMove.x;
|
|
163
|
+
case "right":
|
|
164
|
+
return overlap.x + overlap.width - 1 > userMove.x + userMove.width - 1;
|
|
165
|
+
case "up":
|
|
166
|
+
return overlap.y < userMove.y;
|
|
167
|
+
case "down":
|
|
168
|
+
return overlap.y + overlap.height - 1 > userMove.y + userMove.height - 1;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (conflicts.length > 0) {
|
|
172
|
+
return { direction, items: new Set(conflicts.map((item) => item.id)) };
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
// Applies given move to the solution state by updating the grid, moves, overlaps, and score.
|
|
177
|
+
function makeMove(state, nextMove) {
|
|
178
|
+
updateGridWithMove(state, nextMove);
|
|
179
|
+
updateOverlaps(state, nextMove);
|
|
180
|
+
state.moves.push(nextMove);
|
|
181
|
+
state.score += nextMove.score;
|
|
182
|
+
}
|
|
183
|
+
function updateGridWithMove({ grid }, move) {
|
|
184
|
+
switch (move.type) {
|
|
185
|
+
case "MOVE":
|
|
186
|
+
case "OVERLAP":
|
|
187
|
+
case "FLOAT":
|
|
188
|
+
return grid.move(move.itemId, move.x, move.y);
|
|
189
|
+
case "INSERT":
|
|
190
|
+
return grid.insert({ id: move.itemId, ...move });
|
|
191
|
+
case "REMOVE":
|
|
192
|
+
return grid.remove(move.itemId);
|
|
193
|
+
case "RESIZE":
|
|
194
|
+
return grid.resize(move.itemId, move.width, move.height);
|
|
321
195
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
196
|
+
}
|
|
197
|
+
function updateOverlaps(state, move) {
|
|
198
|
+
var _a;
|
|
199
|
+
// Find and assign items that will overlap with the moved item after the move is performed
|
|
200
|
+
// unless the overlapping items are considered as conflicts.
|
|
201
|
+
for (const newOverlap of state.grid.getOverlaps({ ...move, id: move.itemId })) {
|
|
202
|
+
if (!((_a = state.conflicts) === null || _a === void 0 ? void 0 : _a.items.has(newOverlap.id))) {
|
|
203
|
+
state.overlaps.set(newOverlap.id, move.itemId);
|
|
326
204
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const left = Math.max(0, moveTarget.left - 1);
|
|
333
|
-
for (let y = moveTarget.y; y < moveTarget.y + moveTarget.height; y++) {
|
|
334
|
-
const block = this.grid.getCellOverlap(left, y, moveTarget.id);
|
|
335
|
-
if (block && block.x < left) {
|
|
336
|
-
conflicts.add(block.id);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
case "1:0": {
|
|
342
|
-
const right = Math.min(this.grid.width - 1, moveTarget.right + 1);
|
|
343
|
-
for (let y = moveTarget.y; y < moveTarget.y + moveTarget.height; y++) {
|
|
344
|
-
const block = this.grid.getCellOverlap(right, y, moveTarget.id);
|
|
345
|
-
if (block && block.x + block.width - 1 > right) {
|
|
346
|
-
conflicts.add(block.id);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
case "0:-1": {
|
|
352
|
-
const top = Math.max(0, moveTarget.top - 1);
|
|
353
|
-
for (let x = moveTarget.x; x < moveTarget.x + moveTarget.width; x++) {
|
|
354
|
-
const block = this.grid.getCellOverlap(x, top, moveTarget.id);
|
|
355
|
-
if (block && block.y < top) {
|
|
356
|
-
conflicts.add(block.id);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
case "0:1": {
|
|
362
|
-
const bottom = moveTarget.bottom + 1;
|
|
363
|
-
for (let x = moveTarget.x; x < moveTarget.x + moveTarget.width; x++) {
|
|
364
|
-
const block = this.grid.getCellOverlap(x, bottom, moveTarget.id);
|
|
365
|
-
if (block && block.y + block.height - 1 > bottom) {
|
|
366
|
-
conflicts.add(block.id);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
break;
|
|
370
|
-
}
|
|
371
|
-
default:
|
|
372
|
-
throw new Error(`Invariant violation: unexpected direction ${direction}.`);
|
|
205
|
+
}
|
|
206
|
+
// Remove no longer valid overlaps after the move is performed.
|
|
207
|
+
for (const [overlapId, overlapIssuerId] of state.overlaps) {
|
|
208
|
+
if (!checkItemsIntersection(state.grid.getItem(overlapId), state.grid.getItem(overlapIssuerId))) {
|
|
209
|
+
state.overlaps.delete(overlapId);
|
|
373
210
|
}
|
|
374
|
-
return conflicts;
|
|
375
211
|
}
|
|
376
212
|
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { GridLayout, ItemId } from "../interfaces";
|
|
2
|
+
import { LayoutEngineCacheNode } from "./engine-cache";
|
|
2
3
|
import { InsertCommand, LayoutShift, MoveCommand, ResizeCommand } from "./interfaces";
|
|
4
|
+
/**
|
|
5
|
+
* Layout engine is an abstraction to compute effects of user actions (move, resize, insert, remove).
|
|
6
|
+
* The engine is initialized with the board state and then takes a command to calculate the respective layout shift.
|
|
7
|
+
* Use a single engine instance until the user commits their move to take advantage of the internal cache.
|
|
8
|
+
* Once user move is committed the layout engine needs to be re-initialized with the updated layout state.
|
|
9
|
+
*/
|
|
3
10
|
export declare class LayoutEngine {
|
|
4
|
-
private
|
|
5
|
-
private
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
private cleanup;
|
|
15
|
-
private validateMoveCommand;
|
|
16
|
-
private validateResizeCommand;
|
|
11
|
+
private layout;
|
|
12
|
+
private cache;
|
|
13
|
+
constructor(layout: GridLayout);
|
|
14
|
+
move(moveCommand: MoveCommand, cache?: LayoutEngineCacheNode): LayoutShift;
|
|
15
|
+
resize(resizeCommand: ResizeCommand): LayoutShift;
|
|
16
|
+
insert({ itemId, width, height, path: [position, ...movePath] }: InsertCommand): LayoutShift;
|
|
17
|
+
remove(itemId: ItemId): LayoutShift;
|
|
18
|
+
private getLayoutShift;
|
|
19
|
+
private validateMovePath;
|
|
20
|
+
private validateResizePath;
|
|
17
21
|
}
|