@flighthq/selection 0.4.1-edge.28.7f8e221
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.md +9 -0
- package/README.md +17 -0
- package/dist/contract.d.ts +5 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lassoSelection.d.ts +8 -0
- package/dist/lassoSelection.d.ts.map +1 -0
- package/dist/lassoSelection.js +84 -0
- package/dist/lassoSelection.js.map +1 -0
- package/dist/marqueeSelection.d.ts +8 -0
- package/dist/marqueeSelection.d.ts.map +1 -0
- package/dist/marqueeSelection.js +58 -0
- package/dist/marqueeSelection.js.map +1 -0
- package/dist/pointerSelectionPolicy.d.ts +4 -0
- package/dist/pointerSelectionPolicy.d.ts.map +1 -0
- package/dist/pointerSelectionPolicy.js +35 -0
- package/dist/pointerSelectionPolicy.js.map +1 -0
- package/dist/selectionState.d.ts +15 -0
- package/dist/selectionState.d.ts.map +1 -0
- package/dist/selectionState.js +125 -0
- package/dist/selectionState.js.map +1 -0
- package/package.json +51 -0
- package/src/lassoSelection.test.ts +114 -0
- package/src/marqueeSelection.test.ts +94 -0
- package/src/pointerSelectionPolicy.test.ts +86 -0
- package/src/selectionState.test.ts +199 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2026 Joshua Granick and other contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @flighthq/selection
|
|
2
|
+
|
|
3
|
+
Ordered scene-graph selection state and geometric selection queries
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @flighthq/selection
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Import the supported application-facing API from `@flighthq/selection`. The `@flighthq/selection/contract` export is the wider package-to-package contract used to compose Flight itself.
|
|
12
|
+
|
|
13
|
+
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
|
+
|
|
15
|
+
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
+
- [Source for @flighthq/selection@0.4.1-edge.28.7f8e221](https://github.com/flighthq/flight/tree/7f8e221c72f4afa55c07bf31862b2e3d60e1c81f/packages/selection)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/7f8e221c72f4afa55c07bf31862b2e3d60e1c81f/LICENSE.md)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { addLassoSelectionPoint, addNodeToSelection, applyPointerSelectionPolicy, applyPointerUpSelectionPolicy, beginLassoSelection, beginMarqueeSelection, clearSelection, createLassoSelection, createMarqueeSelection, createSelectionState, endLassoSelection, endMarqueeSelection, findNodesInLassoSelection, findNodesInMarqueeSelection, getActiveNode, getLassoSelectionPath, getMarqueeRectangle, getSelectedNodes, getSelectionCount, getSelectionSignals, hasSelection, isNodeSelected, removeNodeFromSelection, selectAllNodes, selectNode, toggleNodeSelection, updateMarqueeSelection, } from './contract';
|
|
2
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,EAC3B,6BAA6B,EAC7B,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,yBAAyB,EACzB,2BAA2B,EAC3B,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,cAAc,EACd,uBAAuB,EACvB,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { addLassoSelectionPoint, addNodeToSelection, applyPointerSelectionPolicy, applyPointerUpSelectionPolicy, beginLassoSelection, beginMarqueeSelection, clearSelection, createLassoSelection, createMarqueeSelection, createSelectionState, endLassoSelection, endMarqueeSelection, findNodesInLassoSelection, findNodesInMarqueeSelection, getActiveNode, getLassoSelectionPath, getMarqueeRectangle, getSelectedNodes, getSelectionCount, getSelectionSignals, hasSelection, isNodeSelected, removeNodeFromSelection, selectAllNodes, selectNode, toggleNodeSelection, updateMarqueeSelection, } from './contract';
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,EAC3B,6BAA6B,EAC7B,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,yBAAyB,EACzB,2BAA2B,EAC3B,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,cAAc,EACd,uBAAuB,EACvB,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BoundsNodeAny, LassoSelection, Path } from '@flighthq/types/contract';
|
|
2
|
+
export declare function addLassoSelectionPoint(selection: LassoSelection, x: number, y: number): void;
|
|
3
|
+
export declare function beginLassoSelection(selection: LassoSelection, startX: number, startY: number): void;
|
|
4
|
+
export declare function createLassoSelection(): LassoSelection;
|
|
5
|
+
export declare function endLassoSelection(selection: LassoSelection): Path;
|
|
6
|
+
export declare function findNodesInLassoSelection<NodeType extends BoundsNodeAny>(selection: Readonly<LassoSelection>, candidates: readonly NodeType[]): NodeType[];
|
|
7
|
+
export declare function getLassoSelectionPath(selection: Readonly<LassoSelection>): Readonly<Path>;
|
|
8
|
+
//# sourceMappingURL=lassoSelection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lassoSelection.d.ts","sourceRoot":"","sources":["../src/lassoSelection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAyB,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAG3G,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAK5F;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAOnG;AAED,wBAAgB,oBAAoB,IAAI,cAAc,CASrD;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAKjE;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,SAAS,aAAa,EACtE,SAAS,EAAE,QAAQ,CAAC,cAAc,CAAC,EACnC,UAAU,EAAE,SAAS,QAAQ,EAAE,GAC9B,QAAQ,EAAE,CAaZ;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAEzF"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getNodeLocalBoundsRectangle } from '@flighthq/node/contract';
|
|
2
|
+
import { EntityRuntimeKey, PathCommand } from '@flighthq/types/contract';
|
|
3
|
+
export function addLassoSelectionPoint(selection, x, y) {
|
|
4
|
+
const runtime = getLassoSelectionRuntime(selection);
|
|
5
|
+
if (!runtime.active)
|
|
6
|
+
return;
|
|
7
|
+
runtime.path.commands.push(PathCommand.LINE_TO);
|
|
8
|
+
runtime.path.data.push(x, y);
|
|
9
|
+
}
|
|
10
|
+
export function beginLassoSelection(selection, startX, startY) {
|
|
11
|
+
const runtime = getLassoSelectionRuntime(selection);
|
|
12
|
+
runtime.active = true;
|
|
13
|
+
runtime.path.commands.length = 0;
|
|
14
|
+
runtime.path.data.length = 0;
|
|
15
|
+
runtime.path.commands.push(PathCommand.MOVE_TO);
|
|
16
|
+
runtime.path.data.push(startX, startY);
|
|
17
|
+
}
|
|
18
|
+
export function createLassoSelection() {
|
|
19
|
+
const selection = { [EntityRuntimeKey]: undefined };
|
|
20
|
+
const runtime = {
|
|
21
|
+
active: false,
|
|
22
|
+
binding: null,
|
|
23
|
+
path: { commands: [], data: [], winding: 'evenOdd' },
|
|
24
|
+
};
|
|
25
|
+
selection[EntityRuntimeKey] = runtime;
|
|
26
|
+
return selection;
|
|
27
|
+
}
|
|
28
|
+
export function endLassoSelection(selection) {
|
|
29
|
+
const runtime = getLassoSelectionRuntime(selection);
|
|
30
|
+
if (runtime.active)
|
|
31
|
+
runtime.path.commands.push(PathCommand.CLOSE);
|
|
32
|
+
runtime.active = false;
|
|
33
|
+
return runtime.path;
|
|
34
|
+
}
|
|
35
|
+
export function findNodesInLassoSelection(selection, candidates) {
|
|
36
|
+
const path = getLassoSelectionRuntime(selection).path;
|
|
37
|
+
if (path.data.length < 6)
|
|
38
|
+
return [];
|
|
39
|
+
const matches = [];
|
|
40
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
41
|
+
const candidate = candidates[i];
|
|
42
|
+
const bounds = getNodeLocalBoundsRectangle(candidate);
|
|
43
|
+
if (isPointInPolygon(path.data, bounds.x + bounds.width / 2, bounds.y + bounds.height / 2)) {
|
|
44
|
+
matches.push(candidate);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return matches;
|
|
48
|
+
}
|
|
49
|
+
export function getLassoSelectionPath(selection) {
|
|
50
|
+
return getLassoSelectionRuntime(selection).path;
|
|
51
|
+
}
|
|
52
|
+
function getLassoSelectionRuntime(selection) {
|
|
53
|
+
return selection[EntityRuntimeKey];
|
|
54
|
+
}
|
|
55
|
+
function isPointInPolygon(points, x, y) {
|
|
56
|
+
let inside = false;
|
|
57
|
+
const pointCount = points.length / 2;
|
|
58
|
+
let previousIndex = pointCount - 1;
|
|
59
|
+
for (let currentIndex = 0; currentIndex < pointCount; currentIndex++) {
|
|
60
|
+
const currentX = points[currentIndex * 2];
|
|
61
|
+
const currentY = points[currentIndex * 2 + 1];
|
|
62
|
+
const previousX = points[previousIndex * 2];
|
|
63
|
+
const previousY = points[previousIndex * 2 + 1];
|
|
64
|
+
if (isPointOnSegment(x, y, previousX, previousY, currentX, currentY))
|
|
65
|
+
return true;
|
|
66
|
+
if (currentY > y !== previousY > y &&
|
|
67
|
+
x < ((previousX - currentX) * (y - currentY)) / (previousY - currentY) + currentX) {
|
|
68
|
+
inside = !inside;
|
|
69
|
+
}
|
|
70
|
+
previousIndex = currentIndex;
|
|
71
|
+
}
|
|
72
|
+
return inside;
|
|
73
|
+
}
|
|
74
|
+
function isPointOnSegment(x, y, startX, startY, endX, endY) {
|
|
75
|
+
const cross = (x - startX) * (endY - startY) - (y - startY) * (endX - startX);
|
|
76
|
+
const scale = Math.max(1, Math.abs(endX - startX), Math.abs(endY - startY));
|
|
77
|
+
if (Math.abs(cross) > Number.EPSILON * scale * 16)
|
|
78
|
+
return false;
|
|
79
|
+
return (x >= Math.min(startX, endX) &&
|
|
80
|
+
x <= Math.max(startX, endX) &&
|
|
81
|
+
y >= Math.min(startY, endY) &&
|
|
82
|
+
y <= Math.max(startY, endY));
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=lassoSelection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lassoSelection.js","sourceRoot":"","sources":["../src/lassoSelection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BoundsNodeAny, MarqueeSelection, MarqueeSelectionMode, Rectangle } from '@flighthq/types/contract';
|
|
2
|
+
export declare function beginMarqueeSelection(selection: MarqueeSelection, startX: number, startY: number): void;
|
|
3
|
+
export declare function createMarqueeSelection(): MarqueeSelection;
|
|
4
|
+
export declare function endMarqueeSelection(selection: MarqueeSelection): Rectangle;
|
|
5
|
+
export declare function findNodesInMarqueeSelection<NodeType extends BoundsNodeAny>(selection: Readonly<MarqueeSelection>, candidates: readonly NodeType[], mode?: MarqueeSelectionMode): NodeType[];
|
|
6
|
+
export declare function getMarqueeRectangle(selection: Readonly<MarqueeSelection>): Readonly<Rectangle>;
|
|
7
|
+
export declare function updateMarqueeSelection(selection: MarqueeSelection, currentX: number, currentY: number): void;
|
|
8
|
+
//# sourceMappingURL=marqueeSelection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marqueeSelection.d.ts","sourceRoot":"","sources":["../src/marqueeSelection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,oBAAoB,EAEpB,SAAS,EACV,MAAM,0BAA0B,CAAC;AAGlC,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAMvG;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CAWzD;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAI1E;AAED,wBAAgB,2BAA2B,CAAC,QAAQ,SAAS,aAAa,EACxE,SAAS,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EACrC,UAAU,EAAE,SAAS,QAAQ,EAAE,EAC/B,IAAI,GAAE,oBAAkC,GACvC,QAAQ,EAAE,CAWZ;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAE9F;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAI5G"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createRectangle, enclosesRectangle, intersectsRectangle } from '@flighthq/geometry/contract';
|
|
2
|
+
import { getNodeLocalBoundsRectangle } from '@flighthq/node/contract';
|
|
3
|
+
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
4
|
+
export function beginMarqueeSelection(selection, startX, startY) {
|
|
5
|
+
const runtime = getMarqueeSelectionRuntime(selection);
|
|
6
|
+
runtime.active = true;
|
|
7
|
+
runtime.startX = startX;
|
|
8
|
+
runtime.startY = startY;
|
|
9
|
+
setMarqueeRectangle(runtime.rectangle, startX, startY, startX, startY);
|
|
10
|
+
}
|
|
11
|
+
export function createMarqueeSelection() {
|
|
12
|
+
const selection = { [EntityRuntimeKey]: undefined };
|
|
13
|
+
const runtime = {
|
|
14
|
+
active: false,
|
|
15
|
+
binding: null,
|
|
16
|
+
rectangle: createRectangle(),
|
|
17
|
+
startX: 0,
|
|
18
|
+
startY: 0,
|
|
19
|
+
};
|
|
20
|
+
selection[EntityRuntimeKey] = runtime;
|
|
21
|
+
return selection;
|
|
22
|
+
}
|
|
23
|
+
export function endMarqueeSelection(selection) {
|
|
24
|
+
const runtime = getMarqueeSelectionRuntime(selection);
|
|
25
|
+
runtime.active = false;
|
|
26
|
+
return runtime.rectangle;
|
|
27
|
+
}
|
|
28
|
+
export function findNodesInMarqueeSelection(selection, candidates, mode = 'intersect') {
|
|
29
|
+
const rectangle = getMarqueeSelectionRuntime(selection).rectangle;
|
|
30
|
+
const matches = [];
|
|
31
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
32
|
+
const candidate = candidates[i];
|
|
33
|
+
const bounds = getNodeLocalBoundsRectangle(candidate);
|
|
34
|
+
if (mode === 'contain' ? enclosesRectangle(rectangle, bounds) : intersectsRectangle(rectangle, bounds)) {
|
|
35
|
+
matches.push(candidate);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return matches;
|
|
39
|
+
}
|
|
40
|
+
export function getMarqueeRectangle(selection) {
|
|
41
|
+
return getMarqueeSelectionRuntime(selection).rectangle;
|
|
42
|
+
}
|
|
43
|
+
export function updateMarqueeSelection(selection, currentX, currentY) {
|
|
44
|
+
const runtime = getMarqueeSelectionRuntime(selection);
|
|
45
|
+
if (!runtime.active)
|
|
46
|
+
return;
|
|
47
|
+
setMarqueeRectangle(runtime.rectangle, runtime.startX, runtime.startY, currentX, currentY);
|
|
48
|
+
}
|
|
49
|
+
function getMarqueeSelectionRuntime(selection) {
|
|
50
|
+
return selection[EntityRuntimeKey];
|
|
51
|
+
}
|
|
52
|
+
function setMarqueeRectangle(rectangle, startX, startY, endX, endY) {
|
|
53
|
+
rectangle.x = Math.min(startX, endX);
|
|
54
|
+
rectangle.y = Math.min(startY, endY);
|
|
55
|
+
rectangle.width = Math.abs(endX - startX);
|
|
56
|
+
rectangle.height = Math.abs(endY - startY);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=marqueeSelection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marqueeSelection.js","sourceRoot":"","sources":["../src/marqueeSelection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HierarchyNodeAny, SelectionModifierState, SelectionState } from '@flighthq/types/contract';
|
|
2
|
+
export declare function applyPointerSelectionPolicy<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, hit: NodeType | null, modifiers: Readonly<SelectionModifierState>): void;
|
|
3
|
+
export declare function applyPointerUpSelectionPolicy<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, hit: NodeType | null, modifiers: Readonly<SelectionModifierState>, didDrag: boolean): void;
|
|
4
|
+
//# sourceMappingURL=pointerSelectionPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pointerSelectionPolicy.d.ts","sourceRoot":"","sources":["../src/pointerSelectionPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAIzG,wBAAgB,2BAA2B,CAAC,QAAQ,SAAS,gBAAgB,EAC3E,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC/B,GAAG,EAAE,QAAQ,GAAG,IAAI,EACpB,SAAS,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAC1C,IAAI,CAoBN;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,SAAS,gBAAgB,EAC7E,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC/B,GAAG,EAAE,QAAQ,GAAG,IAAI,EACpB,SAAS,EAAE,QAAQ,CAAC,sBAAsB,CAAC,EAC3C,OAAO,EAAE,OAAO,GACf,IAAI,CAGN"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { addNodeToSelection, clearSelection, isNodeSelected, selectNode, toggleNodeSelection } from './selectionState';
|
|
2
|
+
export function applyPointerSelectionPolicy(state, hit, modifiers) {
|
|
3
|
+
// Alt is carried so callers can layer an alternate-pick policy on top. The base selection policy
|
|
4
|
+
// does not give it an implicit mutation that the caller cannot distinguish from an ordinary click.
|
|
5
|
+
if (modifiers.altKey)
|
|
6
|
+
return;
|
|
7
|
+
if (hit === null) {
|
|
8
|
+
if (!hasToggleModifier(modifiers))
|
|
9
|
+
clearSelection(state);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (hasToggleModifier(modifiers)) {
|
|
13
|
+
if (modifiers.shiftKey && !isNodeSelected(state, hit)) {
|
|
14
|
+
addNodeToSelection(state, hit);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
toggleNodeSelection(state, hit);
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!isNodeSelected(state, hit))
|
|
22
|
+
selectNode(state, hit);
|
|
23
|
+
}
|
|
24
|
+
export function applyPointerUpSelectionPolicy(state, hit, modifiers, didDrag) {
|
|
25
|
+
if (didDrag || hit === null || hasAnyModifier(modifiers) || !isNodeSelected(state, hit))
|
|
26
|
+
return;
|
|
27
|
+
selectNode(state, hit);
|
|
28
|
+
}
|
|
29
|
+
function hasAnyModifier(modifiers) {
|
|
30
|
+
return modifiers.altKey || hasToggleModifier(modifiers);
|
|
31
|
+
}
|
|
32
|
+
function hasToggleModifier(modifiers) {
|
|
33
|
+
return modifiers.ctrlKey || modifiers.metaKey || modifiers.shiftKey;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=pointerSelectionPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pointerSelectionPolicy.js","sourceRoot":"","sources":["../src/pointerSelectionPolicy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,cAAc,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvH,MAAM,UAAU,2BAA2B,CACzC,KAA+B,EAC/B,GAAoB,EACpB,SAA2C;IAE3C,iGAAiG;IACjG,mGAAmG;IACnG,IAAI,SAAS,CAAC,MAAM;QAAE,OAAO;IAE7B,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC;YAAE,cAAc,CAAC,KAAK,CAAC,CAAC;QACzD,OAAO;IACT,CAAC;IAED,IAAI,iBAAiB,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,IAAI,SAAS,CAAC,QAAQ,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YACtD,kBAAkB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;aAAM,CAAC;YACN,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAClC,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC;QAAE,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,6BAA6B,CAC3C,KAA+B,EAC/B,GAAoB,EACpB,SAA2C,EAC3C,OAAgB;IAEhB,IAAI,OAAO,IAAI,GAAG,KAAK,IAAI,IAAI,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC;QAAE,OAAO;IAChG,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,SAAS,cAAc,CAAC,SAA2C;IACjE,OAAO,SAAS,CAAC,MAAM,IAAI,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,iBAAiB,CAAC,SAA2C;IACpE,OAAO,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,QAAQ,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HierarchyNodeAny, SelectionSignals, SelectionState } from '@flighthq/types/contract';
|
|
2
|
+
export declare function addNodeToSelection<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, node: NodeType): void;
|
|
3
|
+
export declare function clearSelection<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>): void;
|
|
4
|
+
export declare function createSelectionState<NodeType extends HierarchyNodeAny = HierarchyNodeAny>(): SelectionState<NodeType>;
|
|
5
|
+
export declare function getActiveNode<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>): NodeType | null;
|
|
6
|
+
export declare function getSelectedNodes<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>): readonly NodeType[];
|
|
7
|
+
export declare function getSelectionCount<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>): number;
|
|
8
|
+
export declare function getSelectionSignals<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>): Readonly<SelectionSignals<NodeType>>;
|
|
9
|
+
export declare function hasSelection<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>): boolean;
|
|
10
|
+
export declare function isNodeSelected<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, node: NodeType): boolean;
|
|
11
|
+
export declare function removeNodeFromSelection<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, node: NodeType): void;
|
|
12
|
+
export declare function selectAllNodes<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, candidates: readonly NodeType[]): void;
|
|
13
|
+
export declare function selectNode<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, node: NodeType): void;
|
|
14
|
+
export declare function toggleNodeSelection<NodeType extends HierarchyNodeAny>(state: SelectionState<NodeType>, node: NodeType): void;
|
|
15
|
+
//# sourceMappingURL=selectionState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectionState.d.ts","sourceRoot":"","sources":["../src/selectionState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EAEf,MAAM,0BAA0B,CAAC;AAGlC,wBAAgB,kBAAkB,CAAC,QAAQ,SAAS,gBAAgB,EAClE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC/B,IAAI,EAAE,QAAQ,GACb,IAAI,CASN;AAED,wBAAgB,cAAc,CAAC,QAAQ,SAAS,gBAAgB,EAAE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,IAAI,CASvG;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,SAAS,gBAAgB,GAAG,gBAAgB,KAAK,cAAc,CAAC,QAAQ,CAAC,CAcrH;AAED,wBAAgB,aAAa,CAAC,QAAQ,SAAS,gBAAgB,EAAE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,QAAQ,GAAG,IAAI,CAEjH;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,SAAS,gBAAgB,EAChE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,GAC9B,SAAS,QAAQ,EAAE,CAErB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,SAAS,gBAAgB,EAAE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,MAAM,CAE5G;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,gBAAgB,EACnE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,GAC9B,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAEtC;AAED,wBAAgB,YAAY,CAAC,QAAQ,SAAS,gBAAgB,EAAE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,OAAO,CAExG;AAED,wBAAgB,cAAc,CAAC,QAAQ,SAAS,gBAAgB,EAC9D,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC/B,IAAI,EAAE,QAAQ,GACb,OAAO,CAET;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,SAAS,gBAAgB,EACvE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC/B,IAAI,EAAE,QAAQ,GACb,IAAI,CAWN;AAED,wBAAgB,cAAc,CAAC,QAAQ,SAAS,gBAAgB,EAC9D,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC/B,UAAU,EAAE,SAAS,QAAQ,EAAE,GAC9B,IAAI,CA2BN;AAED,wBAAgB,UAAU,CAAC,QAAQ,SAAS,gBAAgB,EAAE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CASnH;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,gBAAgB,EACnE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC/B,IAAI,EAAE,QAAQ,GACb,IAAI,CAMN"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
2
|
+
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
3
|
+
export function addNodeToSelection(state, node) {
|
|
4
|
+
const runtime = getSelectionStateRuntime(state);
|
|
5
|
+
if (runtime.selectedNodeSet.has(node))
|
|
6
|
+
return;
|
|
7
|
+
const previousActiveNode = runtime.activeNode;
|
|
8
|
+
runtime.selectedNodes.push(node);
|
|
9
|
+
runtime.selectedNodeSet.add(node);
|
|
10
|
+
runtime.activeNode = node;
|
|
11
|
+
emitSelectionChanges(runtime, true, previousActiveNode);
|
|
12
|
+
}
|
|
13
|
+
export function clearSelection(state) {
|
|
14
|
+
const runtime = getSelectionStateRuntime(state);
|
|
15
|
+
if (runtime.selectedNodes.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
const previousActiveNode = runtime.activeNode;
|
|
18
|
+
runtime.selectedNodes = [];
|
|
19
|
+
runtime.selectedNodeSet = new Set();
|
|
20
|
+
runtime.activeNode = null;
|
|
21
|
+
emitSelectionChanges(runtime, true, previousActiveNode);
|
|
22
|
+
}
|
|
23
|
+
export function createSelectionState() {
|
|
24
|
+
const state = { [EntityRuntimeKey]: undefined };
|
|
25
|
+
const runtime = {
|
|
26
|
+
activeNode: null,
|
|
27
|
+
binding: null,
|
|
28
|
+
selectedNodeSet: new Set(),
|
|
29
|
+
selectedNodes: [],
|
|
30
|
+
signals: {
|
|
31
|
+
onActiveChange: createSignal(),
|
|
32
|
+
onChange: createSignal(),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
state[EntityRuntimeKey] = runtime;
|
|
36
|
+
return state;
|
|
37
|
+
}
|
|
38
|
+
export function getActiveNode(state) {
|
|
39
|
+
return getSelectionStateRuntime(state).activeNode;
|
|
40
|
+
}
|
|
41
|
+
export function getSelectedNodes(state) {
|
|
42
|
+
return getSelectionStateRuntime(state).selectedNodes;
|
|
43
|
+
}
|
|
44
|
+
export function getSelectionCount(state) {
|
|
45
|
+
return getSelectionStateRuntime(state).selectedNodes.length;
|
|
46
|
+
}
|
|
47
|
+
export function getSelectionSignals(state) {
|
|
48
|
+
return getSelectionStateRuntime(state).signals;
|
|
49
|
+
}
|
|
50
|
+
export function hasSelection(state) {
|
|
51
|
+
return getSelectionStateRuntime(state).selectedNodes.length !== 0;
|
|
52
|
+
}
|
|
53
|
+
export function isNodeSelected(state, node) {
|
|
54
|
+
return getSelectionStateRuntime(state).selectedNodeSet.has(node);
|
|
55
|
+
}
|
|
56
|
+
export function removeNodeFromSelection(state, node) {
|
|
57
|
+
const runtime = getSelectionStateRuntime(state);
|
|
58
|
+
if (!runtime.selectedNodeSet.has(node))
|
|
59
|
+
return;
|
|
60
|
+
const previousActiveNode = runtime.activeNode;
|
|
61
|
+
runtime.selectedNodeSet.delete(node);
|
|
62
|
+
runtime.selectedNodes.splice(runtime.selectedNodes.indexOf(node), 1);
|
|
63
|
+
if (runtime.activeNode === node) {
|
|
64
|
+
runtime.activeNode = runtime.selectedNodes[runtime.selectedNodes.length - 1] ?? null;
|
|
65
|
+
}
|
|
66
|
+
emitSelectionChanges(runtime, true, previousActiveNode);
|
|
67
|
+
}
|
|
68
|
+
export function selectAllNodes(state, candidates) {
|
|
69
|
+
const runtime = getSelectionStateRuntime(state);
|
|
70
|
+
const selectedNodeSet = new Set();
|
|
71
|
+
const selectedNodes = [];
|
|
72
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
73
|
+
const candidate = candidates[i];
|
|
74
|
+
if (selectedNodeSet.has(candidate))
|
|
75
|
+
continue;
|
|
76
|
+
selectedNodeSet.add(candidate);
|
|
77
|
+
selectedNodes.push(candidate);
|
|
78
|
+
}
|
|
79
|
+
let selectionChanged = runtime.selectedNodes.length !== selectedNodes.length;
|
|
80
|
+
if (!selectionChanged) {
|
|
81
|
+
for (let i = 0; i < selectedNodes.length; i++) {
|
|
82
|
+
if (runtime.selectedNodes[i] !== selectedNodes[i]) {
|
|
83
|
+
selectionChanged = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!selectionChanged)
|
|
89
|
+
return;
|
|
90
|
+
const previousActiveNode = runtime.activeNode;
|
|
91
|
+
runtime.selectedNodeSet = selectedNodeSet;
|
|
92
|
+
runtime.selectedNodes = selectedNodes;
|
|
93
|
+
runtime.activeNode = selectedNodes[selectedNodes.length - 1] ?? null;
|
|
94
|
+
emitSelectionChanges(runtime, true, previousActiveNode);
|
|
95
|
+
}
|
|
96
|
+
export function selectNode(state, node) {
|
|
97
|
+
const runtime = getSelectionStateRuntime(state);
|
|
98
|
+
if (runtime.selectedNodes.length === 1 && runtime.selectedNodes[0] === node)
|
|
99
|
+
return;
|
|
100
|
+
const previousActiveNode = runtime.activeNode;
|
|
101
|
+
runtime.selectedNodeSet = new Set([node]);
|
|
102
|
+
runtime.selectedNodes = [node];
|
|
103
|
+
runtime.activeNode = node;
|
|
104
|
+
emitSelectionChanges(runtime, true, previousActiveNode);
|
|
105
|
+
}
|
|
106
|
+
export function toggleNodeSelection(state, node) {
|
|
107
|
+
if (isNodeSelected(state, node)) {
|
|
108
|
+
removeNodeFromSelection(state, node);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
addNodeToSelection(state, node);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function emitSelectionChanges(runtime, selectionChanged, previousActiveNode) {
|
|
115
|
+
if (selectionChanged && runtime.signals.onChange.data !== null) {
|
|
116
|
+
emitSignal(runtime.signals.onChange, runtime.selectedNodes.slice());
|
|
117
|
+
}
|
|
118
|
+
if (previousActiveNode !== runtime.activeNode) {
|
|
119
|
+
emitSignal(runtime.signals.onActiveChange, runtime.activeNode);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function getSelectionStateRuntime(state) {
|
|
123
|
+
return state[EntityRuntimeKey];
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=selectionState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectionState.js","sourceRoot":"","sources":["../src/selectionState.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/selection",
|
|
3
|
+
"version": "0.4.1-edge.28.7f8e221",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
9
|
+
"directory": "packages/selection"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./contract": {
|
|
20
|
+
"types": "./dist/contract.d.ts",
|
|
21
|
+
"default": "./dist/contract.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src/**/*.test.ts",
|
|
27
|
+
"!dist/**/*.test.js",
|
|
28
|
+
"!dist/**/*.test.d.ts",
|
|
29
|
+
"!dist/**/*.test.js.map",
|
|
30
|
+
"!dist/**/*.test.d.ts.map"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -b",
|
|
34
|
+
"clean": "tsc -b --clean",
|
|
35
|
+
"test": "vitest run --config vitest.config.ts",
|
|
36
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
37
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
38
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@flighthq/geometry": "0.4.1-edge.28.7f8e221",
|
|
42
|
+
"@flighthq/node": "0.4.1-edge.28.7f8e221",
|
|
43
|
+
"@flighthq/signals": "0.4.1-edge.28.7f8e221",
|
|
44
|
+
"@flighthq/types": "0.4.1-edge.28.7f8e221"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"typescript": "^5.3.0"
|
|
48
|
+
},
|
|
49
|
+
"description": "Ordered scene-graph selection state and geometric selection queries",
|
|
50
|
+
"sideEffects": false
|
|
51
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createNode, createNodeRuntime, initBoundsRectangleRuntimeTrait } from '@flighthq/node/contract';
|
|
2
|
+
import type { BoundsNodeAny, HasBoundsRectangleRuntime, NodeRuntime, RectangleLike } from '@flighthq/types/contract';
|
|
3
|
+
import { PathCommand } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
addLassoSelectionPoint,
|
|
7
|
+
beginLassoSelection,
|
|
8
|
+
createLassoSelection,
|
|
9
|
+
endLassoSelection,
|
|
10
|
+
findNodesInLassoSelection,
|
|
11
|
+
getLassoSelectionPath,
|
|
12
|
+
} from './index';
|
|
13
|
+
|
|
14
|
+
describe('addLassoSelectionPoint', () => {
|
|
15
|
+
it('appends line points only during an active gesture', () => {
|
|
16
|
+
const lasso = createLassoSelection();
|
|
17
|
+
addLassoSelectionPoint(lasso, 99, 99);
|
|
18
|
+
beginLassoSelection(lasso, 1, 2);
|
|
19
|
+
addLassoSelectionPoint(lasso, 3, 4);
|
|
20
|
+
endLassoSelection(lasso);
|
|
21
|
+
addLassoSelectionPoint(lasso, 5, 6);
|
|
22
|
+
|
|
23
|
+
expect(getLassoSelectionPath(lasso).data).toEqual([1, 2, 3, 4]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('beginLassoSelection', () => {
|
|
28
|
+
it('resets the reused path and starts it with a move command', () => {
|
|
29
|
+
const lasso = createLassoSelection();
|
|
30
|
+
beginLassoSelection(lasso, 1, 2);
|
|
31
|
+
addLassoSelectionPoint(lasso, 3, 4);
|
|
32
|
+
endLassoSelection(lasso);
|
|
33
|
+
|
|
34
|
+
beginLassoSelection(lasso, 10, 11);
|
|
35
|
+
|
|
36
|
+
expect(getLassoSelectionPath(lasso).commands).toEqual([PathCommand.MOVE_TO]);
|
|
37
|
+
expect(getLassoSelectionPath(lasso).data).toEqual([10, 11]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createLassoSelection', () => {
|
|
42
|
+
it('creates an empty even-odd path', () => {
|
|
43
|
+
const path = getLassoSelectionPath(createLassoSelection());
|
|
44
|
+
|
|
45
|
+
expect(path).toMatchObject({ commands: [], data: [], winding: 'evenOdd' });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('endLassoSelection', () => {
|
|
50
|
+
it('closes an active path once and returns the live path object', () => {
|
|
51
|
+
const lasso = createLassoSelection();
|
|
52
|
+
beginLassoSelection(lasso, 1, 2);
|
|
53
|
+
addLassoSelectionPoint(lasso, 3, 4);
|
|
54
|
+
|
|
55
|
+
const path = endLassoSelection(lasso);
|
|
56
|
+
endLassoSelection(lasso);
|
|
57
|
+
|
|
58
|
+
expect(path).toBe(getLassoSelectionPath(lasso));
|
|
59
|
+
expect(path.commands).toEqual([PathCommand.MOVE_TO, PathCommand.LINE_TO, PathCommand.CLOSE]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('findNodesInLassoSelection', () => {
|
|
64
|
+
it('selects bounds centers inside or on the lasso boundary in caller order', () => {
|
|
65
|
+
const lasso = createLassoSelection();
|
|
66
|
+
beginLassoSelection(lasso, 0, 0);
|
|
67
|
+
addLassoSelectionPoint(lasso, 10, 0);
|
|
68
|
+
addLassoSelectionPoint(lasso, 10, 10);
|
|
69
|
+
addLassoSelectionPoint(lasso, 0, 10);
|
|
70
|
+
endLassoSelection(lasso);
|
|
71
|
+
const inside = createBoundedNode('inside', { x: 4, y: 4, width: 2, height: 2 });
|
|
72
|
+
const boundary = createBoundedNode('boundary', { x: 9, y: 4, width: 2, height: 2 });
|
|
73
|
+
const outside = createBoundedNode('outside', { x: 11, y: 11, width: 2, height: 2 });
|
|
74
|
+
|
|
75
|
+
expect(findNodesInLassoSelection(lasso, [outside, boundary, inside])).toEqual([boundary, inside]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns no candidates for a path with fewer than three points', () => {
|
|
79
|
+
const lasso = createLassoSelection();
|
|
80
|
+
beginLassoSelection(lasso, 0, 0);
|
|
81
|
+
addLassoSelectionPoint(lasso, 10, 0);
|
|
82
|
+
endLassoSelection(lasso);
|
|
83
|
+
|
|
84
|
+
expect(findNodesInLassoSelection(lasso, [createBoundedNode('node', { x: 0, y: 0, width: 1, height: 1 })])).toEqual(
|
|
85
|
+
[],
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('getLassoSelectionPath', () => {
|
|
91
|
+
it('exposes the same path throughout a gesture', () => {
|
|
92
|
+
const lasso = createLassoSelection();
|
|
93
|
+
const path = getLassoSelectionPath(lasso);
|
|
94
|
+
|
|
95
|
+
beginLassoSelection(lasso, 1, 2);
|
|
96
|
+
|
|
97
|
+
expect(getLassoSelectionPath(lasso)).toBe(path);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function createBoundedNode(name: string, rectangle: Readonly<RectangleLike>): BoundsNodeAny {
|
|
102
|
+
return createNode('SelectionBoundsTestNode', { name }, undefined, () => {
|
|
103
|
+
const runtime = createNodeRuntime() as NodeRuntime & HasBoundsRectangleRuntime;
|
|
104
|
+
initBoundsRectangleRuntimeTrait(runtime, {
|
|
105
|
+
computeLocalBoundsRectangle(out) {
|
|
106
|
+
out.x = rectangle.x;
|
|
107
|
+
out.y = rectangle.y;
|
|
108
|
+
out.width = rectangle.width;
|
|
109
|
+
out.height = rectangle.height;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
return runtime;
|
|
113
|
+
}) as BoundsNodeAny;
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createNode, createNodeRuntime, initBoundsRectangleRuntimeTrait } from '@flighthq/node/contract';
|
|
2
|
+
import type { BoundsNodeAny, HasBoundsRectangleRuntime, NodeRuntime, RectangleLike } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
beginMarqueeSelection,
|
|
6
|
+
createMarqueeSelection,
|
|
7
|
+
endMarqueeSelection,
|
|
8
|
+
findNodesInMarqueeSelection,
|
|
9
|
+
getMarqueeRectangle,
|
|
10
|
+
updateMarqueeSelection,
|
|
11
|
+
} from './index';
|
|
12
|
+
|
|
13
|
+
describe('beginMarqueeSelection', () => {
|
|
14
|
+
it('starts a new zero-area rectangle at the pointer location', () => {
|
|
15
|
+
const marquee = createMarqueeSelection();
|
|
16
|
+
beginMarqueeSelection(marquee, 10, 20);
|
|
17
|
+
|
|
18
|
+
expect(getMarqueeRectangle(marquee)).toMatchObject({ x: 10, y: 20, width: 0, height: 0 });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('createMarqueeSelection', () => {
|
|
23
|
+
it('creates an inactive marquee with an empty rectangle', () => {
|
|
24
|
+
const marquee = createMarqueeSelection();
|
|
25
|
+
|
|
26
|
+
updateMarqueeSelection(marquee, 100, 100);
|
|
27
|
+
expect(getMarqueeRectangle(marquee)).toMatchObject({ x: 0, y: 0, width: 0, height: 0 });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('endMarqueeSelection', () => {
|
|
32
|
+
it('returns the live rectangle and makes later updates inert', () => {
|
|
33
|
+
const marquee = createMarqueeSelection();
|
|
34
|
+
beginMarqueeSelection(marquee, 1, 2);
|
|
35
|
+
updateMarqueeSelection(marquee, 3, 4);
|
|
36
|
+
|
|
37
|
+
const rectangle = endMarqueeSelection(marquee);
|
|
38
|
+
updateMarqueeSelection(marquee, 50, 50);
|
|
39
|
+
|
|
40
|
+
expect(rectangle).toBe(getMarqueeRectangle(marquee));
|
|
41
|
+
expect(rectangle).toMatchObject({ x: 1, y: 2, width: 2, height: 2 });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('findNodesInMarqueeSelection', () => {
|
|
46
|
+
it('finds intersecting or fully contained bounds in caller order', () => {
|
|
47
|
+
const marquee = createMarqueeSelection();
|
|
48
|
+
beginMarqueeSelection(marquee, 0, 0);
|
|
49
|
+
updateMarqueeSelection(marquee, 10, 10);
|
|
50
|
+
const contained = createBoundedNode('contained', { x: 2, y: 2, width: 2, height: 2 });
|
|
51
|
+
const overlapping = createBoundedNode('overlapping', { x: 8, y: 8, width: 5, height: 5 });
|
|
52
|
+
const outside = createBoundedNode('outside', { x: 20, y: 20, width: 1, height: 1 });
|
|
53
|
+
|
|
54
|
+
expect(findNodesInMarqueeSelection(marquee, [overlapping, outside, contained])).toEqual([overlapping, contained]);
|
|
55
|
+
expect(findNodesInMarqueeSelection(marquee, [overlapping, outside, contained], 'contain')).toEqual([contained]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('getMarqueeRectangle', () => {
|
|
60
|
+
it('exposes the same rectangle throughout a gesture', () => {
|
|
61
|
+
const marquee = createMarqueeSelection();
|
|
62
|
+
const rectangle = getMarqueeRectangle(marquee);
|
|
63
|
+
|
|
64
|
+
beginMarqueeSelection(marquee, 4, 5);
|
|
65
|
+
|
|
66
|
+
expect(getMarqueeRectangle(marquee)).toBe(rectangle);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('updateMarqueeSelection', () => {
|
|
71
|
+
it('normalizes a reverse drag into positive rectangle dimensions', () => {
|
|
72
|
+
const marquee = createMarqueeSelection();
|
|
73
|
+
beginMarqueeSelection(marquee, 10, 20);
|
|
74
|
+
|
|
75
|
+
updateMarqueeSelection(marquee, 2, 5);
|
|
76
|
+
|
|
77
|
+
expect(getMarqueeRectangle(marquee)).toMatchObject({ x: 2, y: 5, width: 8, height: 15 });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function createBoundedNode(name: string, rectangle: Readonly<RectangleLike>): BoundsNodeAny {
|
|
82
|
+
return createNode('SelectionBoundsTestNode', { name }, undefined, () => {
|
|
83
|
+
const runtime = createNodeRuntime() as NodeRuntime & HasBoundsRectangleRuntime;
|
|
84
|
+
initBoundsRectangleRuntimeTrait(runtime, {
|
|
85
|
+
computeLocalBoundsRectangle(out) {
|
|
86
|
+
out.x = rectangle.x;
|
|
87
|
+
out.y = rectangle.y;
|
|
88
|
+
out.width = rectangle.width;
|
|
89
|
+
out.height = rectangle.height;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return runtime;
|
|
93
|
+
}) as BoundsNodeAny;
|
|
94
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createNode } from '@flighthq/node/contract';
|
|
2
|
+
import type { HierarchyNodeAny, SelectionModifierState } from '@flighthq/types/contract';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
applyPointerSelectionPolicy,
|
|
7
|
+
applyPointerUpSelectionPolicy,
|
|
8
|
+
createSelectionState,
|
|
9
|
+
getSelectedNodes,
|
|
10
|
+
selectAllNodes,
|
|
11
|
+
} from './index';
|
|
12
|
+
|
|
13
|
+
describe('applyPointerSelectionPolicy', () => {
|
|
14
|
+
it('applies unmodified hit and empty-space behavior', () => {
|
|
15
|
+
const first = createTestNode('first');
|
|
16
|
+
const second = createTestNode('second');
|
|
17
|
+
const selection = createSelectionState();
|
|
18
|
+
selectAllNodes(selection, [first, second]);
|
|
19
|
+
|
|
20
|
+
applyPointerSelectionPolicy(selection, first, modifiers());
|
|
21
|
+
expect(getSelectedNodes(selection)).toEqual([first, second]);
|
|
22
|
+
|
|
23
|
+
const third = createTestNode('third');
|
|
24
|
+
applyPointerSelectionPolicy(selection, third, modifiers());
|
|
25
|
+
expect(getSelectedNodes(selection)).toEqual([third]);
|
|
26
|
+
|
|
27
|
+
applyPointerSelectionPolicy(selection, null, modifiers());
|
|
28
|
+
expect(getSelectedNodes(selection)).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('adds or removes with Shift and toggles with Ctrl or Command', () => {
|
|
32
|
+
const first = createTestNode('first');
|
|
33
|
+
const second = createTestNode('second');
|
|
34
|
+
const selection = createSelectionState();
|
|
35
|
+
|
|
36
|
+
applyPointerSelectionPolicy(selection, first, modifiers({ shiftKey: true }));
|
|
37
|
+
expect(getSelectedNodes(selection)).toEqual([first]);
|
|
38
|
+
applyPointerSelectionPolicy(selection, first, modifiers({ shiftKey: true }));
|
|
39
|
+
expect(getSelectedNodes(selection)).toEqual([]);
|
|
40
|
+
|
|
41
|
+
applyPointerSelectionPolicy(selection, first, modifiers({ ctrlKey: true }));
|
|
42
|
+
applyPointerSelectionPolicy(selection, second, modifiers({ metaKey: true }));
|
|
43
|
+
expect(getSelectedNodes(selection)).toEqual([first, second]);
|
|
44
|
+
applyPointerSelectionPolicy(selection, first, modifiers({ metaKey: true }));
|
|
45
|
+
expect(getSelectedNodes(selection)).toEqual([second]);
|
|
46
|
+
|
|
47
|
+
applyPointerSelectionPolicy(selection, null, modifiers({ ctrlKey: true }));
|
|
48
|
+
expect(getSelectedNodes(selection)).toEqual([second]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('leaves Alt gestures available for caller-defined alternate-pick behavior', () => {
|
|
52
|
+
const first = createTestNode('first');
|
|
53
|
+
const second = createTestNode('second');
|
|
54
|
+
const selection = createSelectionState();
|
|
55
|
+
selectAllNodes(selection, [first]);
|
|
56
|
+
|
|
57
|
+
applyPointerSelectionPolicy(selection, second, modifiers({ altKey: true }));
|
|
58
|
+
applyPointerSelectionPolicy(selection, null, modifiers({ altKey: true }));
|
|
59
|
+
|
|
60
|
+
expect(getSelectedNodes(selection)).toEqual([first]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('applyPointerUpSelectionPolicy', () => {
|
|
65
|
+
it('narrows an already-selected hit only on an unmodified pointer-up without a drag', () => {
|
|
66
|
+
const first = createTestNode('first');
|
|
67
|
+
const second = createTestNode('second');
|
|
68
|
+
const selection = createSelectionState();
|
|
69
|
+
selectAllNodes(selection, [first, second]);
|
|
70
|
+
|
|
71
|
+
applyPointerUpSelectionPolicy(selection, first, modifiers(), true);
|
|
72
|
+
applyPointerUpSelectionPolicy(selection, first, modifiers({ shiftKey: true }), false);
|
|
73
|
+
expect(getSelectedNodes(selection)).toEqual([first, second]);
|
|
74
|
+
|
|
75
|
+
applyPointerUpSelectionPolicy(selection, first, modifiers(), false);
|
|
76
|
+
expect(getSelectedNodes(selection)).toEqual([first]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function createTestNode(name: string): HierarchyNodeAny {
|
|
81
|
+
return createNode('SelectionTestNode', { name });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function modifiers(overrides?: Partial<SelectionModifierState>): SelectionModifierState {
|
|
85
|
+
return { altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...overrides };
|
|
86
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { addNodeChild, createNode } from '@flighthq/node/contract';
|
|
2
|
+
import { connectSignal } from '@flighthq/signals/contract';
|
|
3
|
+
import type { HierarchyNodeAny, Node3D, SelectionState } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
addNodeToSelection,
|
|
7
|
+
clearSelection,
|
|
8
|
+
createSelectionState,
|
|
9
|
+
getActiveNode,
|
|
10
|
+
getSelectedNodes,
|
|
11
|
+
getSelectionCount,
|
|
12
|
+
getSelectionSignals,
|
|
13
|
+
hasSelection,
|
|
14
|
+
isNodeSelected,
|
|
15
|
+
removeNodeFromSelection,
|
|
16
|
+
selectAllNodes,
|
|
17
|
+
selectNode,
|
|
18
|
+
toggleNodeSelection,
|
|
19
|
+
} from './index';
|
|
20
|
+
|
|
21
|
+
describe('addNodeToSelection', () => {
|
|
22
|
+
it('adds in order, makes the new node active, and ignores duplicates', () => {
|
|
23
|
+
const first = createTestNode('first');
|
|
24
|
+
const second = createTestNode('second');
|
|
25
|
+
const selection = createSelectionState();
|
|
26
|
+
|
|
27
|
+
addNodeToSelection(selection, first);
|
|
28
|
+
addNodeToSelection(selection, second);
|
|
29
|
+
addNodeToSelection(selection, first);
|
|
30
|
+
|
|
31
|
+
expect(getSelectedNodes(selection)).toEqual([first, second]);
|
|
32
|
+
expect(getActiveNode(selection)).toBe(second);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('clearSelection', () => {
|
|
37
|
+
it('empties the selected set and clears the active node', () => {
|
|
38
|
+
const selection = createSelectionState();
|
|
39
|
+
selectNode(selection, createTestNode('node'));
|
|
40
|
+
|
|
41
|
+
clearSelection(selection);
|
|
42
|
+
|
|
43
|
+
expect(getSelectedNodes(selection)).toEqual([]);
|
|
44
|
+
expect(getActiveNode(selection)).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('createSelectionState', () => {
|
|
49
|
+
it('creates empty state that accepts any hierarchy graph family', () => {
|
|
50
|
+
const selection = createSelectionState();
|
|
51
|
+
|
|
52
|
+
expect(getSelectedNodes(selection)).toEqual([]);
|
|
53
|
+
expectTypeOf(createSelectionState<Node3D>()).toMatchTypeOf<SelectionState<Node3D>>();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('getActiveNode', () => {
|
|
58
|
+
it('returns the most recently selected remaining node', () => {
|
|
59
|
+
const first = createTestNode('first');
|
|
60
|
+
const second = createTestNode('second');
|
|
61
|
+
const selection = createSelectionState();
|
|
62
|
+
selectAllNodes(selection, [first, second]);
|
|
63
|
+
|
|
64
|
+
removeNodeFromSelection(selection, second);
|
|
65
|
+
|
|
66
|
+
expect(getActiveNode(selection)).toBe(first);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('getSelectedNodes', () => {
|
|
71
|
+
it('returns the ordered selected identities', () => {
|
|
72
|
+
const first = createTestNode('first');
|
|
73
|
+
const second = createTestNode('second');
|
|
74
|
+
const selection = createSelectionState();
|
|
75
|
+
selectAllNodes(selection, [second, first]);
|
|
76
|
+
|
|
77
|
+
expect(getSelectedNodes(selection)).toEqual([second, first]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('getSelectionCount', () => {
|
|
82
|
+
it('returns the selected identity count', () => {
|
|
83
|
+
const selection = createSelectionState();
|
|
84
|
+
selectAllNodes(selection, [createTestNode('first'), createTestNode('second')]);
|
|
85
|
+
|
|
86
|
+
expect(getSelectionCount(selection)).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('getSelectionSignals', () => {
|
|
91
|
+
it('emits snapshots after finalizing state and suppresses no-op emissions', () => {
|
|
92
|
+
const first = createTestNode('first');
|
|
93
|
+
const second = createTestNode('second');
|
|
94
|
+
const selection = createSelectionState();
|
|
95
|
+
const changes: (readonly HierarchyNodeAny[])[] = [];
|
|
96
|
+
const onChange = vi.fn((selected: readonly HierarchyNodeAny[]) => {
|
|
97
|
+
changes.push(selected);
|
|
98
|
+
expect(getSelectedNodes(selection)).toEqual(selected);
|
|
99
|
+
});
|
|
100
|
+
const onActiveChange = vi.fn();
|
|
101
|
+
const signals = getSelectionSignals(selection);
|
|
102
|
+
connectSignal(signals.onChange, onChange);
|
|
103
|
+
connectSignal(signals.onActiveChange, onActiveChange);
|
|
104
|
+
|
|
105
|
+
addNodeToSelection(selection, first);
|
|
106
|
+
addNodeToSelection(selection, first);
|
|
107
|
+
addNodeToSelection(selection, second);
|
|
108
|
+
removeNodeFromSelection(selection, first);
|
|
109
|
+
clearSelection(selection);
|
|
110
|
+
clearSelection(selection);
|
|
111
|
+
|
|
112
|
+
expect(onChange).toHaveBeenCalledTimes(4);
|
|
113
|
+
expect(onActiveChange).toHaveBeenCalledTimes(3);
|
|
114
|
+
expect(changes).toEqual([[first], [first, second], [second], []]);
|
|
115
|
+
expect(changes[1]).not.toBe(getSelectedNodes(selection));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('hasSelection', () => {
|
|
120
|
+
it('reports whether any identity is selected', () => {
|
|
121
|
+
const selection = createSelectionState();
|
|
122
|
+
expect(hasSelection(selection)).toBe(false);
|
|
123
|
+
|
|
124
|
+
selectNode(selection, createTestNode('node'));
|
|
125
|
+
expect(hasSelection(selection)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('isNodeSelected', () => {
|
|
130
|
+
it('checks selection by node identity', () => {
|
|
131
|
+
const selected = createTestNode('selected');
|
|
132
|
+
const unselected = createTestNode('unselected');
|
|
133
|
+
const selection = createSelectionState();
|
|
134
|
+
selectNode(selection, selected);
|
|
135
|
+
|
|
136
|
+
expect(isNodeSelected(selection, selected)).toBe(true);
|
|
137
|
+
expect(isNodeSelected(selection, unselected)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('removeNodeFromSelection', () => {
|
|
142
|
+
it('preserves an unrelated active node and ignores absent identities', () => {
|
|
143
|
+
const first = createTestNode('first');
|
|
144
|
+
const second = createTestNode('second');
|
|
145
|
+
const absent = createTestNode('absent');
|
|
146
|
+
const selection = createSelectionState();
|
|
147
|
+
selectAllNodes(selection, [first, second]);
|
|
148
|
+
|
|
149
|
+
removeNodeFromSelection(selection, first);
|
|
150
|
+
removeNodeFromSelection(selection, absent);
|
|
151
|
+
|
|
152
|
+
expect(getSelectedNodes(selection)).toEqual([second]);
|
|
153
|
+
expect(getActiveNode(selection)).toBe(second);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('selectAllNodes', () => {
|
|
158
|
+
it('de-duplicates candidates in caller order and makes the last unique node active', () => {
|
|
159
|
+
const first = createTestNode('first');
|
|
160
|
+
const second = createTestNode('second');
|
|
161
|
+
const selection = createSelectionState();
|
|
162
|
+
|
|
163
|
+
selectAllNodes(selection, [second, first, second]);
|
|
164
|
+
|
|
165
|
+
expect(getSelectedNodes(selection)).toEqual([second, first]);
|
|
166
|
+
expect(getActiveNode(selection)).toBe(first);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('selectNode', () => {
|
|
171
|
+
it('replaces selection without recursively selecting descendants', () => {
|
|
172
|
+
const group = createTestNode('group');
|
|
173
|
+
const child = createTestNode('child');
|
|
174
|
+
addNodeChild(group, child);
|
|
175
|
+
const selection = createSelectionState();
|
|
176
|
+
selectNode(selection, child);
|
|
177
|
+
|
|
178
|
+
selectNode(selection, group);
|
|
179
|
+
|
|
180
|
+
expect(getSelectedNodes(selection)).toEqual([group]);
|
|
181
|
+
expect(isNodeSelected(selection, child)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('toggleNodeSelection', () => {
|
|
186
|
+
it('adds an absent identity and removes a selected identity', () => {
|
|
187
|
+
const node = createTestNode('node');
|
|
188
|
+
const selection = createSelectionState();
|
|
189
|
+
|
|
190
|
+
toggleNodeSelection(selection, node);
|
|
191
|
+
expect(isNodeSelected(selection, node)).toBe(true);
|
|
192
|
+
toggleNodeSelection(selection, node);
|
|
193
|
+
expect(isNodeSelected(selection, node)).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
function createTestNode(name: string): HierarchyNodeAny {
|
|
198
|
+
return createNode('SelectionTestNode', { name });
|
|
199
|
+
}
|