react-native-layout-dnd 0.1.0-alpha.0
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/LICENSE +20 -0
- package/README.md +262 -0
- package/lib/module/adapters/coordinates.js +135 -0
- package/lib/module/adapters/coordinates.js.map +1 -0
- package/lib/module/adapters/scroll.js +81 -0
- package/lib/module/adapters/scroll.js.map +1 -0
- package/lib/module/adapters/virtualizedList.js +69 -0
- package/lib/module/adapters/virtualizedList.js.map +1 -0
- package/lib/module/components/DndProvider.js +1144 -0
- package/lib/module/components/DndProvider.js.map +1 -0
- package/lib/module/components/DragHandle.js +65 -0
- package/lib/module/components/DragHandle.js.map +1 -0
- package/lib/module/components/GridController.js +151 -0
- package/lib/module/components/GridController.js.map +1 -0
- package/lib/module/components/GridDragHandle.js +15 -0
- package/lib/module/components/GridDragHandle.js.map +1 -0
- package/lib/module/components/GridZoneView.js +59 -0
- package/lib/module/components/GridZoneView.js.map +1 -0
- package/lib/module/components/SortableGrid.js +305 -0
- package/lib/module/components/SortableGrid.js.map +1 -0
- package/lib/module/components/SortableList.js +714 -0
- package/lib/module/components/SortableList.js.map +1 -0
- package/lib/module/components/dndTypes.js +4 -0
- package/lib/module/components/dndTypes.js.map +1 -0
- package/lib/module/components/gridTypes.js +4 -0
- package/lib/module/components/gridTypes.js.map +1 -0
- package/lib/module/components/handleViewProps.js +13 -0
- package/lib/module/components/handleViewProps.js.map +1 -0
- package/lib/module/contracts.js +4 -0
- package/lib/module/contracts.js.map +1 -0
- package/lib/module/controller/approval.js +98 -0
- package/lib/module/controller/approval.js.map +1 -0
- package/lib/module/controller/coordinatorBase.js +93 -0
- package/lib/module/controller/coordinatorBase.js.map +1 -0
- package/lib/module/controller/dndSession.js +621 -0
- package/lib/module/controller/dndSession.js.map +1 -0
- package/lib/module/controller/dndState.js +103 -0
- package/lib/module/controller/dndState.js.map +1 -0
- package/lib/module/controller/gridAdapter.js +191 -0
- package/lib/module/controller/gridAdapter.js.map +1 -0
- package/lib/module/controller/gridState.js +101 -0
- package/lib/module/controller/gridState.js.map +1 -0
- package/lib/module/controller/locations.js +48 -0
- package/lib/module/controller/locations.js.map +1 -0
- package/lib/module/controller/previewPath.js +73 -0
- package/lib/module/controller/previewPath.js.map +1 -0
- package/lib/module/controller/sessionCore.js +222 -0
- package/lib/module/controller/sessionCore.js.map +1 -0
- package/lib/module/controller/useDndState.js +9 -0
- package/lib/module/controller/useDndState.js.map +1 -0
- package/lib/module/controller/useGridState.js +9 -0
- package/lib/module/controller/useGridState.js.map +1 -0
- package/lib/module/controller/useStoreState.js +15 -0
- package/lib/module/controller/useStoreState.js.map +1 -0
- package/lib/module/defaults.js +7 -0
- package/lib/module/defaults.js.map +1 -0
- package/lib/module/diagnostics.js +16 -0
- package/lib/module/diagnostics.js.map +1 -0
- package/lib/module/engine/gridPlacements.js +54 -0
- package/lib/module/engine/gridPlacements.js.map +1 -0
- package/lib/module/engine/index.js +19 -0
- package/lib/module/engine/index.js.map +1 -0
- package/lib/module/engine/layout.js +36 -0
- package/lib/module/engine/layout.js.map +1 -0
- package/lib/module/engine/layoutMove.js +326 -0
- package/lib/module/engine/layoutMove.js.map +1 -0
- package/lib/module/engine/layoutState.js +344 -0
- package/lib/module/engine/layoutState.js.map +1 -0
- package/lib/module/engine/list.js +172 -0
- package/lib/module/engine/list.js.map +1 -0
- package/lib/module/engine/listIndex.js +125 -0
- package/lib/module/engine/listIndex.js.map +1 -0
- package/lib/module/engine/move.js +216 -0
- package/lib/module/engine/move.js.map +1 -0
- package/lib/module/engine/movementPolicy.js +77 -0
- package/lib/module/engine/movementPolicy.js.map +1 -0
- package/lib/module/engine/orderedTarget.js +71 -0
- package/lib/module/engine/orderedTarget.js.map +1 -0
- package/lib/module/engine/pack.js +30 -0
- package/lib/module/engine/pack.js.map +1 -0
- package/lib/module/engine/rectangles.js +12 -0
- package/lib/module/engine/rectangles.js.map +1 -0
- package/lib/module/engine/sameLayout.js +29 -0
- package/lib/module/engine/sameLayout.js.map +1 -0
- package/lib/module/engine/search.js +159 -0
- package/lib/module/engine/search.js.map +1 -0
- package/lib/module/engine/spatial.js +27 -0
- package/lib/module/engine/spatial.js.map +1 -0
- package/lib/module/engine/spatialAppRotation.js +39 -0
- package/lib/module/engine/spatialAppRotation.js.map +1 -0
- package/lib/module/engine/spatialExchange.js +164 -0
- package/lib/module/engine/spatialExchange.js.map +1 -0
- package/lib/module/engine/spatialInsertion.js +135 -0
- package/lib/module/engine/spatialInsertion.js.map +1 -0
- package/lib/module/engine/spatialPolicy.js +41 -0
- package/lib/module/engine/spatialPolicy.js.map +1 -0
- package/lib/module/engine/spatialRotation.js +48 -0
- package/lib/module/engine/spatialRotation.js.map +1 -0
- package/lib/module/engine/validation.js +205 -0
- package/lib/module/engine/validation.js.map +1 -0
- package/lib/module/gesture/DndGestureGroup.js +268 -0
- package/lib/module/gesture/DndGestureGroup.js.map +1 -0
- package/lib/module/gesture/DndGestureHost.js +32 -0
- package/lib/module/gesture/DndGestureHost.js.map +1 -0
- package/lib/module/gesture/gestureConfig.js +81 -0
- package/lib/module/gesture/gestureConfig.js.map +1 -0
- package/lib/module/gesture/gestureGrouping.js +122 -0
- package/lib/module/gesture/gestureGrouping.js.map +1 -0
- package/lib/module/gesture/gestureHost.js +106 -0
- package/lib/module/gesture/gestureHost.js.map +1 -0
- package/lib/module/index.js +23 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/pointer/dndTarget.js +101 -0
- package/lib/module/pointer/dndTarget.js.map +1 -0
- package/lib/module/pointer/geometry.js +363 -0
- package/lib/module/pointer/geometry.js.map +1 -0
- package/lib/module/pointer/layoutTarget.js +307 -0
- package/lib/module/pointer/layoutTarget.js.map +1 -0
- package/lib/module/pointer/listCellMotion.js +99 -0
- package/lib/module/pointer/listCellMotion.js.map +1 -0
- package/lib/module/pointer/listTarget.js +42 -0
- package/lib/module/pointer/listTarget.js.map +1 -0
- package/lib/module/pointer/motion.js +37 -0
- package/lib/module/pointer/motion.js.map +1 -0
- package/lib/module/runtime/dndPresentation.js +81 -0
- package/lib/module/runtime/dndPresentation.js.map +1 -0
- package/lib/module/runtime/dndRenderStore.js +129 -0
- package/lib/module/runtime/dndRenderStore.js.map +1 -0
- package/lib/module/runtime/dndRuntime.js +135 -0
- package/lib/module/runtime/dndRuntime.js.map +1 -0
- package/lib/module/runtime/dndSettlement.js +77 -0
- package/lib/module/runtime/dndSettlement.js.map +1 -0
- package/lib/module/runtime/gridRuntime.js +37 -0
- package/lib/module/runtime/gridRuntime.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/adapters/coordinates.d.ts +9 -0
- package/lib/typescript/src/adapters/coordinates.d.ts.map +1 -0
- package/lib/typescript/src/adapters/scroll.d.ts +49 -0
- package/lib/typescript/src/adapters/scroll.d.ts.map +1 -0
- package/lib/typescript/src/adapters/virtualizedList.d.ts +22 -0
- package/lib/typescript/src/adapters/virtualizedList.d.ts.map +1 -0
- package/lib/typescript/src/components/DndProvider.d.ts +4 -0
- package/lib/typescript/src/components/DndProvider.d.ts.map +1 -0
- package/lib/typescript/src/components/DragHandle.d.ts +4 -0
- package/lib/typescript/src/components/DragHandle.d.ts.map +1 -0
- package/lib/typescript/src/components/GridController.d.ts +21 -0
- package/lib/typescript/src/components/GridController.d.ts.map +1 -0
- package/lib/typescript/src/components/GridDragHandle.d.ts +8 -0
- package/lib/typescript/src/components/GridDragHandle.d.ts.map +1 -0
- package/lib/typescript/src/components/GridZoneView.d.ts +8 -0
- package/lib/typescript/src/components/GridZoneView.d.ts.map +1 -0
- package/lib/typescript/src/components/SortableGrid.d.ts +4 -0
- package/lib/typescript/src/components/SortableGrid.d.ts.map +1 -0
- package/lib/typescript/src/components/SortableList.d.ts +4 -0
- package/lib/typescript/src/components/SortableList.d.ts.map +1 -0
- package/lib/typescript/src/components/dndTypes.d.ts +94 -0
- package/lib/typescript/src/components/dndTypes.d.ts.map +1 -0
- package/lib/typescript/src/components/gridTypes.d.ts +84 -0
- package/lib/typescript/src/components/gridTypes.d.ts.map +1 -0
- package/lib/typescript/src/components/handleViewProps.d.ts +6 -0
- package/lib/typescript/src/components/handleViewProps.d.ts.map +1 -0
- package/lib/typescript/src/contracts.d.ts +145 -0
- package/lib/typescript/src/contracts.d.ts.map +1 -0
- package/lib/typescript/src/controller/approval.d.ts +66 -0
- package/lib/typescript/src/controller/approval.d.ts.map +1 -0
- package/lib/typescript/src/controller/coordinatorBase.d.ts +37 -0
- package/lib/typescript/src/controller/coordinatorBase.d.ts.map +1 -0
- package/lib/typescript/src/controller/dndSession.d.ts +152 -0
- package/lib/typescript/src/controller/dndSession.d.ts.map +1 -0
- package/lib/typescript/src/controller/dndState.d.ts +27 -0
- package/lib/typescript/src/controller/dndState.d.ts.map +1 -0
- package/lib/typescript/src/controller/gridAdapter.d.ts +44 -0
- package/lib/typescript/src/controller/gridAdapter.d.ts.map +1 -0
- package/lib/typescript/src/controller/gridState.d.ts +18 -0
- package/lib/typescript/src/controller/gridState.d.ts.map +1 -0
- package/lib/typescript/src/controller/locations.d.ts +9 -0
- package/lib/typescript/src/controller/locations.d.ts.map +1 -0
- package/lib/typescript/src/controller/previewPath.d.ts +49 -0
- package/lib/typescript/src/controller/previewPath.d.ts.map +1 -0
- package/lib/typescript/src/controller/sessionCore.d.ts +106 -0
- package/lib/typescript/src/controller/sessionCore.d.ts.map +1 -0
- package/lib/typescript/src/controller/useDndState.d.ts +8 -0
- package/lib/typescript/src/controller/useDndState.d.ts.map +1 -0
- package/lib/typescript/src/controller/useGridState.d.ts +8 -0
- package/lib/typescript/src/controller/useGridState.d.ts.map +1 -0
- package/lib/typescript/src/controller/useStoreState.d.ts +16 -0
- package/lib/typescript/src/controller/useStoreState.d.ts.map +1 -0
- package/lib/typescript/src/defaults.d.ts +5 -0
- package/lib/typescript/src/defaults.d.ts.map +1 -0
- package/lib/typescript/src/diagnostics.d.ts +9 -0
- package/lib/typescript/src/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/engine/gridPlacements.d.ts +22 -0
- package/lib/typescript/src/engine/gridPlacements.d.ts.map +1 -0
- package/lib/typescript/src/engine/index.d.ts +28 -0
- package/lib/typescript/src/engine/index.d.ts.map +1 -0
- package/lib/typescript/src/engine/layout.d.ts +4 -0
- package/lib/typescript/src/engine/layout.d.ts.map +1 -0
- package/lib/typescript/src/engine/layoutMove.d.ts +46 -0
- package/lib/typescript/src/engine/layoutMove.d.ts.map +1 -0
- package/lib/typescript/src/engine/layoutState.d.ts +22 -0
- package/lib/typescript/src/engine/layoutState.d.ts.map +1 -0
- package/lib/typescript/src/engine/list.d.ts +68 -0
- package/lib/typescript/src/engine/list.d.ts.map +1 -0
- package/lib/typescript/src/engine/listIndex.d.ts +32 -0
- package/lib/typescript/src/engine/listIndex.d.ts.map +1 -0
- package/lib/typescript/src/engine/move.d.ts +10 -0
- package/lib/typescript/src/engine/move.d.ts.map +1 -0
- package/lib/typescript/src/engine/movementPolicy.d.ts +34 -0
- package/lib/typescript/src/engine/movementPolicy.d.ts.map +1 -0
- package/lib/typescript/src/engine/orderedTarget.d.ts +32 -0
- package/lib/typescript/src/engine/orderedTarget.d.ts.map +1 -0
- package/lib/typescript/src/engine/pack.d.ts +3 -0
- package/lib/typescript/src/engine/pack.d.ts.map +1 -0
- package/lib/typescript/src/engine/rectangles.d.ts +9 -0
- package/lib/typescript/src/engine/rectangles.d.ts.map +1 -0
- package/lib/typescript/src/engine/sameLayout.d.ts +4 -0
- package/lib/typescript/src/engine/sameLayout.d.ts.map +1 -0
- package/lib/typescript/src/engine/search.d.ts +28 -0
- package/lib/typescript/src/engine/search.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatial.d.ts +16 -0
- package/lib/typescript/src/engine/spatial.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialAppRotation.d.ts +4 -0
- package/lib/typescript/src/engine/spatialAppRotation.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialExchange.d.ts +5 -0
- package/lib/typescript/src/engine/spatialExchange.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialInsertion.d.ts +5 -0
- package/lib/typescript/src/engine/spatialInsertion.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialPolicy.d.ts +9 -0
- package/lib/typescript/src/engine/spatialPolicy.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialRotation.d.ts +8 -0
- package/lib/typescript/src/engine/spatialRotation.d.ts.map +1 -0
- package/lib/typescript/src/engine/validation.d.ts +9 -0
- package/lib/typescript/src/engine/validation.d.ts.map +1 -0
- package/lib/typescript/src/gesture/DndGestureGroup.d.ts +33 -0
- package/lib/typescript/src/gesture/DndGestureGroup.d.ts.map +1 -0
- package/lib/typescript/src/gesture/DndGestureHost.d.ts +12 -0
- package/lib/typescript/src/gesture/DndGestureHost.d.ts.map +1 -0
- package/lib/typescript/src/gesture/gestureConfig.d.ts +15 -0
- package/lib/typescript/src/gesture/gestureConfig.d.ts.map +1 -0
- package/lib/typescript/src/gesture/gestureGrouping.d.ts +66 -0
- package/lib/typescript/src/gesture/gestureGrouping.d.ts.map +1 -0
- package/lib/typescript/src/gesture/gestureHost.d.ts +23 -0
- package/lib/typescript/src/gesture/gestureHost.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +38 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/pointer/dndTarget.d.ts +14 -0
- package/lib/typescript/src/pointer/dndTarget.d.ts.map +1 -0
- package/lib/typescript/src/pointer/geometry.d.ts +28 -0
- package/lib/typescript/src/pointer/geometry.d.ts.map +1 -0
- package/lib/typescript/src/pointer/layoutTarget.d.ts +43 -0
- package/lib/typescript/src/pointer/layoutTarget.d.ts.map +1 -0
- package/lib/typescript/src/pointer/listCellMotion.d.ts +43 -0
- package/lib/typescript/src/pointer/listCellMotion.d.ts.map +1 -0
- package/lib/typescript/src/pointer/listTarget.d.ts +7 -0
- package/lib/typescript/src/pointer/listTarget.d.ts.map +1 -0
- package/lib/typescript/src/pointer/motion.d.ts +20 -0
- package/lib/typescript/src/pointer/motion.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndPresentation.d.ts +37 -0
- package/lib/typescript/src/runtime/dndPresentation.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndRenderStore.d.ts +46 -0
- package/lib/typescript/src/runtime/dndRenderStore.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndRuntime.d.ts +138 -0
- package/lib/typescript/src/runtime/dndRuntime.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndSettlement.d.ts +18 -0
- package/lib/typescript/src/runtime/dndSettlement.d.ts.map +1 -0
- package/lib/typescript/src/runtime/gridRuntime.d.ts +54 -0
- package/lib/typescript/src/runtime/gridRuntime.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +180 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +136 -0
- package/src/adapters/coordinates.ts +181 -0
- package/src/adapters/scroll.ts +147 -0
- package/src/adapters/virtualizedList.ts +112 -0
- package/src/components/DndProvider.tsx +1836 -0
- package/src/components/DragHandle.tsx +89 -0
- package/src/components/GridController.tsx +213 -0
- package/src/components/GridDragHandle.tsx +11 -0
- package/src/components/GridZoneView.tsx +57 -0
- package/src/components/SortableGrid.tsx +449 -0
- package/src/components/SortableList.tsx +1028 -0
- package/src/components/dndTypes.ts +102 -0
- package/src/components/gridTypes.ts +90 -0
- package/src/components/handleViewProps.ts +35 -0
- package/src/contracts.ts +149 -0
- package/src/controller/approval.ts +179 -0
- package/src/controller/coordinatorBase.ts +118 -0
- package/src/controller/dndSession.ts +1058 -0
- package/src/controller/dndState.ts +198 -0
- package/src/controller/gridAdapter.ts +268 -0
- package/src/controller/gridState.ts +187 -0
- package/src/controller/locations.ts +67 -0
- package/src/controller/previewPath.ts +90 -0
- package/src/controller/sessionCore.ts +390 -0
- package/src/controller/useDndState.ts +26 -0
- package/src/controller/useGridState.ts +22 -0
- package/src/controller/useStoreState.ts +27 -0
- package/src/defaults.ts +4 -0
- package/src/diagnostics.ts +23 -0
- package/src/engine/gridPlacements.ts +85 -0
- package/src/engine/index.ts +99 -0
- package/src/engine/layout.ts +29 -0
- package/src/engine/layoutMove.ts +401 -0
- package/src/engine/layoutState.ts +490 -0
- package/src/engine/list.ts +307 -0
- package/src/engine/listIndex.ts +174 -0
- package/src/engine/move.ts +274 -0
- package/src/engine/movementPolicy.ts +151 -0
- package/src/engine/orderedTarget.ts +131 -0
- package/src/engine/pack.ts +24 -0
- package/src/engine/rectangles.ts +18 -0
- package/src/engine/sameLayout.ts +50 -0
- package/src/engine/search.ts +174 -0
- package/src/engine/spatial.ts +36 -0
- package/src/engine/spatialAppRotation.ts +53 -0
- package/src/engine/spatialExchange.ts +212 -0
- package/src/engine/spatialInsertion.ts +157 -0
- package/src/engine/spatialPolicy.ts +48 -0
- package/src/engine/spatialRotation.ts +56 -0
- package/src/engine/validation.ts +358 -0
- package/src/gesture/DndGestureGroup.tsx +357 -0
- package/src/gesture/DndGestureHost.tsx +50 -0
- package/src/gesture/gestureConfig.ts +123 -0
- package/src/gesture/gestureGrouping.ts +210 -0
- package/src/gesture/gestureHost.tsx +163 -0
- package/src/index.ts +162 -0
- package/src/pointer/dndTarget.ts +213 -0
- package/src/pointer/geometry.ts +584 -0
- package/src/pointer/layoutTarget.ts +515 -0
- package/src/pointer/listCellMotion.ts +174 -0
- package/src/pointer/listTarget.ts +56 -0
- package/src/pointer/motion.ts +66 -0
- package/src/runtime/dndPresentation.ts +113 -0
- package/src/runtime/dndRenderStore.ts +246 -0
- package/src/runtime/dndRuntime.ts +311 -0
- package/src/runtime/dndSettlement.ts +117 -0
- package/src/runtime/gridRuntime.ts +73 -0
- package/src/types.ts +189 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { CellPosition, GridItem, PositionedItem } from '../types';
|
|
2
|
+
|
|
3
|
+
import { rectanglesOverlap } from './rectangles';
|
|
4
|
+
|
|
5
|
+
export type PlacementSearchResult<T> =
|
|
6
|
+
| { status: 'ok'; placements: PositionedItem<T>[]; attempts: number }
|
|
7
|
+
| { status: 'impossible' | 'unresolved'; attempts: number };
|
|
8
|
+
type PlacementSearchProgress<T> =
|
|
9
|
+
PlacementSearchResult<T> | { status: 'pending'; attempts: number };
|
|
10
|
+
|
|
11
|
+
type SearchInput<T> = {
|
|
12
|
+
items: readonly GridItem<T>[];
|
|
13
|
+
fixedItems?: readonly PositionedItem<T>[];
|
|
14
|
+
rows: number;
|
|
15
|
+
columns: number;
|
|
16
|
+
searchBudget: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function* rowMajorCandidates(
|
|
20
|
+
maxRow: number,
|
|
21
|
+
maxCol: number,
|
|
22
|
+
): Generator<CellPosition> {
|
|
23
|
+
for (let row = 0; row <= maxRow; row++) {
|
|
24
|
+
for (let col = 0; col <= maxCol; col++) {
|
|
25
|
+
yield { row, col };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Distance shells, then row/column order. No grid-sized array or sort. */
|
|
31
|
+
export function* nearestCandidates(
|
|
32
|
+
maxRow: number,
|
|
33
|
+
maxCol: number,
|
|
34
|
+
origin: CellPosition,
|
|
35
|
+
): Generator<CellPosition> {
|
|
36
|
+
const colReach = Math.max(origin.col, maxCol - origin.col);
|
|
37
|
+
const maxDistance = Math.min(
|
|
38
|
+
Number.MAX_SAFE_INTEGER,
|
|
39
|
+
Math.max(origin.row, maxRow - origin.row) + colReach,
|
|
40
|
+
);
|
|
41
|
+
for (let distance = 0; distance <= maxDistance; distance++) {
|
|
42
|
+
const minRowDistance = Math.max(0, distance - colReach);
|
|
43
|
+
const firstRow = distance > origin.row ? 0 : origin.row - distance;
|
|
44
|
+
const lastRow = origin.row + Math.min(distance, maxRow - origin.row);
|
|
45
|
+
for (let row = firstRow; row <= lastRow; row++) {
|
|
46
|
+
// Skip the middle of a wide shell when the grid has very few columns.
|
|
47
|
+
if (Math.abs(row - origin.row) < minRowDistance) {
|
|
48
|
+
if (minRowDistance > maxRow - origin.row) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
row = origin.row + minRowDistance;
|
|
52
|
+
}
|
|
53
|
+
if (row > lastRow) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const colDistance = distance - Math.abs(row - origin.row);
|
|
57
|
+
if (colDistance <= origin.col) {
|
|
58
|
+
yield { row, col: origin.col - colDistance };
|
|
59
|
+
}
|
|
60
|
+
if (colDistance > 0 && colDistance <= maxCol - origin.col) {
|
|
61
|
+
yield { row, col: origin.col + colDistance };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function exceedsArea<T>(
|
|
68
|
+
items: readonly GridItem<T>[],
|
|
69
|
+
fixedItems: readonly PositionedItem<T>[],
|
|
70
|
+
rows: number,
|
|
71
|
+
columns: number,
|
|
72
|
+
): boolean {
|
|
73
|
+
let remaining = rows * columns;
|
|
74
|
+
// Area is only a shortcut. Avoid a rounded product becoming a proof.
|
|
75
|
+
if (!Number.isSafeInteger(remaining)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
for (const group of [fixedItems, items]) {
|
|
79
|
+
for (const item of group) {
|
|
80
|
+
const area = item.span.rows * item.span.cols;
|
|
81
|
+
if (area > remaining) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
remaining -= area;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Internal: inputs are validated by the caller; fixed rectangles never move. */
|
|
91
|
+
export function createPlacementSearch<T>({
|
|
92
|
+
items,
|
|
93
|
+
fixedItems = [],
|
|
94
|
+
rows,
|
|
95
|
+
columns,
|
|
96
|
+
searchBudget,
|
|
97
|
+
}: SearchInput<T>): {
|
|
98
|
+
advance: (maxAttempts: number) => PlacementSearchProgress<T>;
|
|
99
|
+
} {
|
|
100
|
+
let attempts = 0;
|
|
101
|
+
let depth = 0;
|
|
102
|
+
let terminal: PlacementSearchResult<T> | undefined;
|
|
103
|
+
const placements: PositionedItem<T>[] = [];
|
|
104
|
+
const cursors: Generator<CellPosition>[] = [];
|
|
105
|
+
let nextPosition: CellPosition | undefined;
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
items.some(item => item.span.rows > rows || item.span.cols > columns) ||
|
|
109
|
+
exceedsArea(items, fixedItems, rows, columns)
|
|
110
|
+
) {
|
|
111
|
+
terminal = { status: 'impossible', attempts };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createCursor(index: number): Generator<CellPosition> {
|
|
115
|
+
const item = items[index];
|
|
116
|
+
const maxRow = rows - item.span.rows;
|
|
117
|
+
const maxCol = columns - item.span.cols;
|
|
118
|
+
return rowMajorCandidates(maxRow, maxCol);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
advance(maxAttempts) {
|
|
123
|
+
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 0) {
|
|
124
|
+
throw new RangeError(
|
|
125
|
+
'Search chunk size must be a nonnegative safe integer.',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
let chunkAttempts = 0;
|
|
129
|
+
while (!terminal) {
|
|
130
|
+
if (depth === items.length) {
|
|
131
|
+
terminal = { status: 'ok', placements, attempts };
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
cursors[depth] ??= createCursor(depth);
|
|
135
|
+
if (!nextPosition) {
|
|
136
|
+
const candidate = cursors[depth].next();
|
|
137
|
+
if (candidate.done) {
|
|
138
|
+
// Exhaustion is free to inspect: at an exact budget boundary this
|
|
139
|
+
// distinguishes a proof of impossibility from unfinished search.
|
|
140
|
+
cursors.pop();
|
|
141
|
+
if (depth === 0) {
|
|
142
|
+
terminal = { status: 'impossible', attempts };
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
depth--;
|
|
146
|
+
placements.pop();
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
nextPosition = candidate.value;
|
|
150
|
+
}
|
|
151
|
+
if (attempts === searchBudget) {
|
|
152
|
+
terminal = { status: 'unresolved', attempts };
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (chunkAttempts === maxAttempts) {
|
|
156
|
+
return { status: 'pending', attempts };
|
|
157
|
+
}
|
|
158
|
+
attempts++;
|
|
159
|
+
chunkAttempts++;
|
|
160
|
+
const candidate = { ...items[depth], position: nextPosition };
|
|
161
|
+
nextPosition = undefined;
|
|
162
|
+
if (
|
|
163
|
+
fixedItems.some(item => rectanglesOverlap(candidate, item)) ||
|
|
164
|
+
placements.some(item => rectanglesOverlap(candidate, item))
|
|
165
|
+
) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
placements.push(candidate);
|
|
169
|
+
depth++;
|
|
170
|
+
}
|
|
171
|
+
return terminal;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PositionedItem } from '../types';
|
|
2
|
+
|
|
3
|
+
import type { GridMovementPolicy } from './movementPolicy';
|
|
4
|
+
import type { PlacementSearchResult } from './search';
|
|
5
|
+
import { exchangeSpatialRegions } from './spatialExchange';
|
|
6
|
+
import { insertSpatialItem } from './spatialInsertion';
|
|
7
|
+
import { rotateSpatialRows } from './spatialRotation';
|
|
8
|
+
import { selectSpatialMove } from './spatialPolicy';
|
|
9
|
+
|
|
10
|
+
export type SpatialMoveInput<T> = {
|
|
11
|
+
items: readonly PositionedItem<T>[];
|
|
12
|
+
active: PositionedItem<T>;
|
|
13
|
+
/** Original active item, only when the source and target are the same zone. */
|
|
14
|
+
origin?: PositionedItem<T>;
|
|
15
|
+
rows: number;
|
|
16
|
+
columns: number;
|
|
17
|
+
searchBudget: number;
|
|
18
|
+
movementPolicy?: GridMovementPolicy;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Internal: target input is valid, and items excludes the active item. */
|
|
22
|
+
export function resolveSpatialMove<T>(
|
|
23
|
+
input: SpatialMoveInput<T>,
|
|
24
|
+
): PlacementSearchResult<T> {
|
|
25
|
+
const { move, kind, colliders } = selectSpatialMove(input);
|
|
26
|
+
if (kind === 'row-rotation') return rotateSpatialRows(move)!;
|
|
27
|
+
if (kind === 'insert') return insertSpatialItem(move, colliders);
|
|
28
|
+
if (kind === 'exchange') return exchangeSpatialRegions(move);
|
|
29
|
+
if (kind === 'vacancy')
|
|
30
|
+
return {
|
|
31
|
+
status: 'ok',
|
|
32
|
+
placements: [...move.items, move.active],
|
|
33
|
+
attempts: 0,
|
|
34
|
+
};
|
|
35
|
+
return { status: 'impossible', attempts: 0 };
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { rectanglesOverlap } from './rectangles';
|
|
2
|
+
import type { SpatialMoveInput } from './spatial';
|
|
3
|
+
|
|
4
|
+
/** Resolve an adjacent app drag through the same move as dragging its widget. */
|
|
5
|
+
export function normalizeAppWidgetMove<T>(
|
|
6
|
+
input: SpatialMoveInput<T>,
|
|
7
|
+
): SpatialMoveInput<T> | null {
|
|
8
|
+
'worklet';
|
|
9
|
+
const { items, active, origin } = input;
|
|
10
|
+
if (
|
|
11
|
+
!origin ||
|
|
12
|
+
active.placement !== 'insert' ||
|
|
13
|
+
active.span.rows !== 1 ||
|
|
14
|
+
active.span.cols !== 1
|
|
15
|
+
)
|
|
16
|
+
return null;
|
|
17
|
+
|
|
18
|
+
const widget = items.find(
|
|
19
|
+
item => item.placement !== 'insert' && rectanglesOverlap(active, item),
|
|
20
|
+
);
|
|
21
|
+
if (!widget) return null;
|
|
22
|
+
const withinColumns =
|
|
23
|
+
origin.position.col >= widget.position.col &&
|
|
24
|
+
origin.position.col < widget.position.col + widget.span.cols;
|
|
25
|
+
const withinRows =
|
|
26
|
+
origin.position.row >= widget.position.row &&
|
|
27
|
+
origin.position.row < widget.position.row + widget.span.rows;
|
|
28
|
+
const above =
|
|
29
|
+
withinColumns && origin.position.row === widget.position.row - 1;
|
|
30
|
+
const below =
|
|
31
|
+
withinColumns &&
|
|
32
|
+
origin.position.row === widget.position.row + widget.span.rows;
|
|
33
|
+
const left = withinRows && origin.position.col === widget.position.col - 1;
|
|
34
|
+
const right =
|
|
35
|
+
withinRows &&
|
|
36
|
+
origin.position.col === widget.position.col + widget.span.cols;
|
|
37
|
+
if (!above && !below && !left && !right) return null;
|
|
38
|
+
|
|
39
|
+
// Restore the picked-up app before resolving the widget's one-cell move.
|
|
40
|
+
// The ordinary widget policy then owns neighbor movement and boundary checks.
|
|
41
|
+
return {
|
|
42
|
+
...input,
|
|
43
|
+
items: [...items.filter(item => item !== widget), origin],
|
|
44
|
+
origin: widget,
|
|
45
|
+
active: {
|
|
46
|
+
...widget,
|
|
47
|
+
position: {
|
|
48
|
+
row: widget.position.row + (above ? -1 : below ? 1 : 0),
|
|
49
|
+
col: widget.position.col + (left ? -1 : right ? 1 : 0),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { CellPosition, CellSpan, PositionedItem } from '../types';
|
|
2
|
+
|
|
3
|
+
import { rectanglesOverlap } from './rectangles';
|
|
4
|
+
import { nearestCandidates, type PlacementSearchResult } from './search';
|
|
5
|
+
import type { SpatialMoveInput } from './spatial';
|
|
6
|
+
|
|
7
|
+
type Region = { position: CellPosition; span: CellSpan };
|
|
8
|
+
|
|
9
|
+
function contains(region: Region, item: Region): boolean {
|
|
10
|
+
return (
|
|
11
|
+
item.position.row >= region.position.row &&
|
|
12
|
+
item.position.col >= region.position.col &&
|
|
13
|
+
item.position.row - region.position.row <=
|
|
14
|
+
region.span.rows - item.span.rows &&
|
|
15
|
+
item.position.col - region.position.col <= region.span.cols - item.span.cols
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Include complete target items; expanding one edge may encounter another item. */
|
|
20
|
+
function closeTargetRegion<T>(
|
|
21
|
+
active: PositionedItem<T>,
|
|
22
|
+
items: readonly PositionedItem<T>[],
|
|
23
|
+
): Region {
|
|
24
|
+
let region: Region = { position: active.position, span: active.span };
|
|
25
|
+
let expanded = true;
|
|
26
|
+
while (expanded) {
|
|
27
|
+
expanded = false;
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
if (!rectanglesOverlap(region, item) || contains(region, item)) continue;
|
|
30
|
+
const row = Math.min(region.position.row, item.position.row);
|
|
31
|
+
const col = Math.min(region.position.col, item.position.col);
|
|
32
|
+
// Validated items and the active target fit in the zone, so ends stay safe.
|
|
33
|
+
const endRow = Math.max(
|
|
34
|
+
region.position.row + region.span.rows,
|
|
35
|
+
item.position.row + item.span.rows,
|
|
36
|
+
);
|
|
37
|
+
const endCol = Math.max(
|
|
38
|
+
region.position.col + region.span.cols,
|
|
39
|
+
item.position.col + item.span.cols,
|
|
40
|
+
);
|
|
41
|
+
region = {
|
|
42
|
+
position: { row, col },
|
|
43
|
+
span: { rows: endRow - row, cols: endCol - col },
|
|
44
|
+
};
|
|
45
|
+
expanded = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return region;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function translated<T>(
|
|
52
|
+
item: PositionedItem<T>,
|
|
53
|
+
from: Region,
|
|
54
|
+
to: Region,
|
|
55
|
+
): PositionedItem<T> {
|
|
56
|
+
return {
|
|
57
|
+
...item,
|
|
58
|
+
position: {
|
|
59
|
+
row: to.position.row + (item.position.row - from.position.row),
|
|
60
|
+
col: to.position.col + (item.position.col - from.position.col),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Rotate entering items through an overlapping translation into the vacated edge.
|
|
67
|
+
* A whole item must follow one cycle; an item whose cells would split is rejected.
|
|
68
|
+
*/
|
|
69
|
+
function exchangeOverlappingRegions<T>(
|
|
70
|
+
items: readonly PositionedItem<T>[],
|
|
71
|
+
active: PositionedItem<T>,
|
|
72
|
+
origin: PositionedItem<T>,
|
|
73
|
+
target: Region,
|
|
74
|
+
): PlacementSearchResult<T> {
|
|
75
|
+
const attempts = 1;
|
|
76
|
+
// Expanded mixed-item regions have no unambiguous overlapping source region.
|
|
77
|
+
if (
|
|
78
|
+
target.position.row !== active.position.row ||
|
|
79
|
+
target.position.col !== active.position.col ||
|
|
80
|
+
target.span.rows !== active.span.rows ||
|
|
81
|
+
target.span.cols !== active.span.cols
|
|
82
|
+
)
|
|
83
|
+
return { status: 'impossible', attempts };
|
|
84
|
+
|
|
85
|
+
const rowStep = origin.position.row - active.position.row;
|
|
86
|
+
const colStep = origin.position.col - active.position.col;
|
|
87
|
+
const placements: PositionedItem<T>[] = [];
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
if (!rectanglesOverlap(target, item)) {
|
|
90
|
+
placements.push(item);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Compute the first translation wholly outside the target, without a loop
|
|
94
|
+
// proportional to grid dimensions (which may be very large safe integers).
|
|
95
|
+
const rowSteps =
|
|
96
|
+
rowStep > 0
|
|
97
|
+
? Math.ceil(
|
|
98
|
+
(target.position.row + target.span.rows - item.position.row) /
|
|
99
|
+
rowStep,
|
|
100
|
+
)
|
|
101
|
+
: rowStep < 0
|
|
102
|
+
? Math.ceil(
|
|
103
|
+
(item.position.row + item.span.rows - target.position.row) /
|
|
104
|
+
-rowStep,
|
|
105
|
+
)
|
|
106
|
+
: Infinity;
|
|
107
|
+
const colSteps =
|
|
108
|
+
colStep > 0
|
|
109
|
+
? Math.ceil(
|
|
110
|
+
(target.position.col + target.span.cols - item.position.col) /
|
|
111
|
+
colStep,
|
|
112
|
+
)
|
|
113
|
+
: colStep < 0
|
|
114
|
+
? Math.ceil(
|
|
115
|
+
(item.position.col + item.span.cols - target.position.col) /
|
|
116
|
+
-colStep,
|
|
117
|
+
)
|
|
118
|
+
: Infinity;
|
|
119
|
+
const steps = Math.min(rowSteps, colSteps);
|
|
120
|
+
const moved = {
|
|
121
|
+
...item,
|
|
122
|
+
position: {
|
|
123
|
+
row: item.position.row + steps * rowStep,
|
|
124
|
+
col: item.position.col + steps * colStep,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
if (
|
|
128
|
+
!Number.isSafeInteger(moved.position.row) ||
|
|
129
|
+
!Number.isSafeInteger(moved.position.col) ||
|
|
130
|
+
!contains(origin, moved) ||
|
|
131
|
+
rectanglesOverlap(target, moved)
|
|
132
|
+
)
|
|
133
|
+
return { status: 'impossible', attempts };
|
|
134
|
+
// Containment in origin = target + step also means the entire previous
|
|
135
|
+
// translation was inside target. Thus no part of this item exits early.
|
|
136
|
+
// These cycles are disjoint and origin contains no other original items.
|
|
137
|
+
placements.push(moved);
|
|
138
|
+
}
|
|
139
|
+
placements.push(active);
|
|
140
|
+
return { status: 'ok', placements, attempts };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Exchange equal regions, preserving whole items and their empty cells. */
|
|
144
|
+
export function exchangeSpatialRegions<T>({
|
|
145
|
+
items,
|
|
146
|
+
active,
|
|
147
|
+
origin,
|
|
148
|
+
rows,
|
|
149
|
+
columns,
|
|
150
|
+
searchBudget,
|
|
151
|
+
}: SpatialMoveInput<T>): PlacementSearchResult<T> {
|
|
152
|
+
if (!origin) return { status: 'impossible', attempts: 0 };
|
|
153
|
+
const target = closeTargetRegion(active, items);
|
|
154
|
+
if (rectanglesOverlap(origin, active))
|
|
155
|
+
return exchangeOverlappingRegions(items, active, origin, target);
|
|
156
|
+
const firstRow = Math.max(
|
|
157
|
+
0,
|
|
158
|
+
origin.position.row - (target.span.rows - origin.span.rows),
|
|
159
|
+
);
|
|
160
|
+
const firstCol = Math.max(
|
|
161
|
+
0,
|
|
162
|
+
origin.position.col - (target.span.cols - origin.span.cols),
|
|
163
|
+
);
|
|
164
|
+
const lastRow = Math.min(origin.position.row, rows - target.span.rows);
|
|
165
|
+
const lastCol = Math.min(origin.position.col, columns - target.span.cols);
|
|
166
|
+
// Choosing a source near this anchor puts the active item near its requested
|
|
167
|
+
// target. Clamping each axis adds a constant distance to every valid candidate.
|
|
168
|
+
const preferred = {
|
|
169
|
+
row: origin.position.row + (target.position.row - active.position.row),
|
|
170
|
+
col: origin.position.col + (target.position.col - active.position.col),
|
|
171
|
+
};
|
|
172
|
+
const candidates = nearestCandidates(lastRow - firstRow, lastCol - firstCol, {
|
|
173
|
+
row: Math.max(firstRow, Math.min(lastRow, preferred.row)) - firstRow,
|
|
174
|
+
col: Math.max(firstCol, Math.min(lastCol, preferred.col)) - firstCol,
|
|
175
|
+
});
|
|
176
|
+
let attempts = 0;
|
|
177
|
+
for (const position of candidates) {
|
|
178
|
+
if (attempts === searchBudget) return { status: 'unresolved', attempts };
|
|
179
|
+
attempts++;
|
|
180
|
+
const source: Region = {
|
|
181
|
+
position: {
|
|
182
|
+
row: firstRow + position.row,
|
|
183
|
+
col: firstCol + position.col,
|
|
184
|
+
},
|
|
185
|
+
span: target.span,
|
|
186
|
+
};
|
|
187
|
+
if (rectanglesOverlap(source, target)) continue;
|
|
188
|
+
if (
|
|
189
|
+
items.some(
|
|
190
|
+
item => rectanglesOverlap(source, item) && !contains(source, item),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
continue;
|
|
194
|
+
|
|
195
|
+
// Each touched item is wholly contained in exactly one disjoint region.
|
|
196
|
+
// Translation therefore cannot collide with fixed items or lose any holes.
|
|
197
|
+
const placements = items.map(item => {
|
|
198
|
+
if (contains(source, item)) return translated(item, source, target);
|
|
199
|
+
if (contains(target, item)) return translated(item, target, source);
|
|
200
|
+
return item;
|
|
201
|
+
});
|
|
202
|
+
const moved = translated(origin, source, target);
|
|
203
|
+
placements.push(
|
|
204
|
+
moved.position.row === active.position.row &&
|
|
205
|
+
moved.position.col === active.position.col
|
|
206
|
+
? active
|
|
207
|
+
: moved,
|
|
208
|
+
);
|
|
209
|
+
return { status: 'ok', placements, attempts };
|
|
210
|
+
}
|
|
211
|
+
return { status: 'impossible', attempts };
|
|
212
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { CellPosition, CellSpan, PositionedItem } from '../types';
|
|
2
|
+
|
|
3
|
+
import { rectanglesOverlap } from './rectangles';
|
|
4
|
+
import type { PlacementSearchResult } from './search';
|
|
5
|
+
import type { SpatialMoveInput } from './spatial';
|
|
6
|
+
|
|
7
|
+
function comparePositions(left: CellPosition, right: CellPosition): number {
|
|
8
|
+
return left.row - right.row || left.col - right.col;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isSlotAligned(item: PositionedItem<unknown>, span: CellSpan): boolean {
|
|
12
|
+
return (
|
|
13
|
+
item.span.rows === span.rows &&
|
|
14
|
+
item.span.cols === span.cols &&
|
|
15
|
+
item.position.row % span.rows === 0 &&
|
|
16
|
+
item.position.col % span.cols === 0
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function withPosition<T>(
|
|
21
|
+
item: PositionedItem<T>,
|
|
22
|
+
position: CellPosition,
|
|
23
|
+
): PositionedItem<T> {
|
|
24
|
+
return { ...item, position: { ...position } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function applyInsertion<T>(
|
|
28
|
+
items: readonly PositionedItem<T>[],
|
|
29
|
+
active: PositionedItem<T>,
|
|
30
|
+
moved: ReadonlyMap<string, PositionedItem<T>>,
|
|
31
|
+
attempts: number,
|
|
32
|
+
): PlacementSearchResult<T> {
|
|
33
|
+
return {
|
|
34
|
+
status: 'ok',
|
|
35
|
+
placements: [...items.map(item => moved.get(item.id) ?? item), active],
|
|
36
|
+
attempts,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Shift intervening occupied slots; keep the requested target even when it was empty. */
|
|
41
|
+
function insertWithinZone<T>(
|
|
42
|
+
{
|
|
43
|
+
items,
|
|
44
|
+
active,
|
|
45
|
+
origin,
|
|
46
|
+
}: SpatialMoveInput<T> & {
|
|
47
|
+
origin: PositionedItem<T>;
|
|
48
|
+
},
|
|
49
|
+
vacantTarget = false,
|
|
50
|
+
): PlacementSearchResult<T> {
|
|
51
|
+
const forward = comparePositions(origin.position, active.position) < 0;
|
|
52
|
+
const start = forward ? origin.position : active.position;
|
|
53
|
+
const end = forward ? active.position : origin.position;
|
|
54
|
+
const displaced = items
|
|
55
|
+
.filter(
|
|
56
|
+
item =>
|
|
57
|
+
item.placement === 'insert' &&
|
|
58
|
+
comparePositions(item.position, start) >= 0 &&
|
|
59
|
+
comparePositions(item.position, end) <= 0,
|
|
60
|
+
)
|
|
61
|
+
.sort((left, right) =>
|
|
62
|
+
forward
|
|
63
|
+
? comparePositions(left.position, right.position)
|
|
64
|
+
: comparePositions(right.position, left.position),
|
|
65
|
+
);
|
|
66
|
+
if (
|
|
67
|
+
!isSlotAligned(active, active.span) ||
|
|
68
|
+
!isSlotAligned(origin, active.span) ||
|
|
69
|
+
displaced.some(item => !isSlotAligned(item, active.span))
|
|
70
|
+
) {
|
|
71
|
+
// An empty target remains usable even when larger insertion items do not
|
|
72
|
+
// share aligned slots. Only compatible slots participate in insertion.
|
|
73
|
+
if (vacantTarget)
|
|
74
|
+
return { status: 'ok', placements: [...items, active], attempts: 0 };
|
|
75
|
+
return { status: 'impossible', attempts: 1 };
|
|
76
|
+
}
|
|
77
|
+
let vacant = origin.position;
|
|
78
|
+
const moved = new Map<string, PositionedItem<T>>();
|
|
79
|
+
for (const item of displaced) {
|
|
80
|
+
moved.set(item.id, withPosition(item, vacant));
|
|
81
|
+
vacant = item.position;
|
|
82
|
+
}
|
|
83
|
+
return applyInsertion(items, active, moved, 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** An incoming item shifts insertion slots forward until a free slot is reached. */
|
|
87
|
+
function insertFromAnotherZone<T>({
|
|
88
|
+
items,
|
|
89
|
+
active,
|
|
90
|
+
rows,
|
|
91
|
+
columns,
|
|
92
|
+
searchBudget,
|
|
93
|
+
}: SpatialMoveInput<T>): PlacementSearchResult<T> {
|
|
94
|
+
const displaced: PositionedItem<T>[] = [];
|
|
95
|
+
const maxRow = rows - active.span.rows;
|
|
96
|
+
const maxCol = columns - active.span.cols;
|
|
97
|
+
let { row, col } = active.position;
|
|
98
|
+
let attempts = 0;
|
|
99
|
+
while (row <= maxRow) {
|
|
100
|
+
if (attempts === searchBudget) return { status: 'unresolved', attempts };
|
|
101
|
+
attempts++;
|
|
102
|
+
const slot = { span: active.span, position: { row, col } };
|
|
103
|
+
const occupants = items.filter(item => rectanglesOverlap(item, slot));
|
|
104
|
+
if (occupants.length === 0) {
|
|
105
|
+
const moved = new Map<string, PositionedItem<T>>();
|
|
106
|
+
for (let index = 0; index < displaced.length; index++) {
|
|
107
|
+
moved.set(
|
|
108
|
+
displaced[index].id,
|
|
109
|
+
withPosition(
|
|
110
|
+
displaced[index],
|
|
111
|
+
displaced[index + 1]?.position ?? slot.position,
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return applyInsertion(items, active, moved, attempts);
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
occupants.some(
|
|
119
|
+
item =>
|
|
120
|
+
item.placement === 'insert' && !isSlotAligned(item, active.span),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return { status: 'impossible', attempts };
|
|
124
|
+
if (occupants.length === 1 && occupants[0].placement === 'insert') {
|
|
125
|
+
displaced.push(occupants[0]);
|
|
126
|
+
}
|
|
127
|
+
// Do not multiply the number of rows and columns or increment past safe ends.
|
|
128
|
+
if (col <= maxCol - active.span.cols) {
|
|
129
|
+
col += active.span.cols;
|
|
130
|
+
} else if (row <= maxRow - active.span.rows) {
|
|
131
|
+
row += active.span.rows;
|
|
132
|
+
col = 0;
|
|
133
|
+
} else {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { status: 'impossible', attempts };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function insertSpatialItem<T>(
|
|
141
|
+
input: SpatialMoveInput<T>,
|
|
142
|
+
colliders: readonly PositionedItem<T>[],
|
|
143
|
+
): PlacementSearchResult<T> {
|
|
144
|
+
const { active, origin } = input;
|
|
145
|
+
if (origin && colliders.length === 0)
|
|
146
|
+
return insertWithinZone({ ...input, origin }, true);
|
|
147
|
+
if (
|
|
148
|
+
colliders.length !== 1 ||
|
|
149
|
+
!isSlotAligned(active, active.span) ||
|
|
150
|
+
!isSlotAligned(colliders[0], active.span) ||
|
|
151
|
+
comparePositions(colliders[0].position, active.position) !== 0
|
|
152
|
+
)
|
|
153
|
+
return { status: 'impossible', attempts: 0 };
|
|
154
|
+
return origin
|
|
155
|
+
? insertWithinZone({ ...input, origin })
|
|
156
|
+
: insertFromAnotherZone(input);
|
|
157
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DEFAULT_GRID_MOVEMENT_POLICY } from './movementPolicy';
|
|
2
|
+
import type { GridMoveCandidate } from './movementPolicy';
|
|
3
|
+
import { rectanglesOverlap } from './rectangles';
|
|
4
|
+
import type { SpatialMoveInput } from './spatial';
|
|
5
|
+
import { normalizeAppWidgetMove } from './spatialAppRotation';
|
|
6
|
+
|
|
7
|
+
/** Shared rule selection only: no packing, search, or caller callbacks on the UI thread. */
|
|
8
|
+
export function selectSpatialMove<T>(input: SpatialMoveInput<T>) {
|
|
9
|
+
'worklet';
|
|
10
|
+
const policy = input.movementPolicy ?? DEFAULT_GRID_MOVEMENT_POLICY;
|
|
11
|
+
const move =
|
|
12
|
+
policy.adjacentInsertExchange === 'single-cell'
|
|
13
|
+
? (normalizeAppWidgetMove(input) ?? input)
|
|
14
|
+
: input;
|
|
15
|
+
const { active, origin, items, columns } = move;
|
|
16
|
+
const colliders = items.filter(item => rectanglesOverlap(active, item));
|
|
17
|
+
let kind: GridMoveCandidate | 'vacancy' | 'blocked' =
|
|
18
|
+
colliders.length === 0 ? 'vacancy' : 'blocked';
|
|
19
|
+
for (const candidate of policy.candidateOrder ??
|
|
20
|
+
DEFAULT_GRID_MOVEMENT_POLICY.candidateOrder) {
|
|
21
|
+
if (
|
|
22
|
+
candidate === 'row-rotation' &&
|
|
23
|
+
policy.rowRotation === 'full-width' &&
|
|
24
|
+
origin &&
|
|
25
|
+
active.placement !== 'insert' &&
|
|
26
|
+
active.span.cols === columns &&
|
|
27
|
+
active.position.col === origin.position.col &&
|
|
28
|
+
active.position.row !== origin.position.row
|
|
29
|
+
) {
|
|
30
|
+
kind = candidate;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
candidate === 'insert' &&
|
|
35
|
+
active.placement === 'insert' &&
|
|
36
|
+
(origin !== undefined || colliders.length > 0) &&
|
|
37
|
+
colliders.every(item => item.placement === 'insert')
|
|
38
|
+
) {
|
|
39
|
+
kind = candidate;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (candidate === 'exchange' && colliders.length > 0) {
|
|
43
|
+
kind = candidate;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { move, kind, colliders };
|
|
48
|
+
}
|