optimized selection via selectionbox, implemented proper defaultEdge/-NodeOptions

This commit is contained in:
peterkogo
2024-12-19 17:33:16 +01:00
parent 19e233af2d
commit 6349290722
10 changed files with 78 additions and 60 deletions
@@ -27,8 +27,8 @@
targetY, targetY,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
animated, animated = false,
selected, selected = false,
label, label,
labelStyle, labelStyle,
data = {}, data = {},
@@ -40,12 +40,12 @@
markerStart, markerStart,
markerEnd, markerEnd,
selectable: edgeSelectable, selectable: edgeSelectable,
deletable, deletable = true,
hidden, hidden,
zIndex, zIndex,
class: className, class: className,
ariaLabel ariaLabel
} = $derived(edge); } = $derived(store.defaultEdgeOptions ? { ...store.defaultEdgeOptions, ...edge } : edge);
const { id } = edge; const { id } = edge;
setContext('svelteflow__edge_id', id); setContext('svelteflow__edge_id', id);
@@ -64,7 +64,7 @@
const edge = store.edgeLookup.get(id); const edge = store.edgeLookup.get(id);
if (edge) { if (edge) {
store.handleEdgeSelection(id); if (selectable) store.handleEdgeSelection(id);
onedgeclick?.({ event, edge }); onedgeclick?.({ event, edge });
} }
} }
@@ -79,6 +79,8 @@
callback({ event, edge }); callback({ event, edge });
} }
} }
$inspect(selected);
</script> </script>
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -132,7 +134,7 @@
{style} {style}
{interactionWidth} {interactionWidth}
{selectable} {selectable}
deletable={deletable ?? true} {deletable}
{type} {type}
sourceHandleId={sourceHandle} sourceHandleId={sourceHandle}
targetHandleId={targetHandle} targetHandleId={targetHandle}
@@ -40,7 +40,6 @@
); );
let store = useStore(); let store = useStore();
// let { edges } = store;
function onPointerDown(event: MouseEvent | TouchEvent) { function onPointerDown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event); const isMouseTriggered = isMouseEvent(event);
@@ -91,7 +90,6 @@
$effect.pre(() => { $effect.pre(() => {
// connectionLookup is not reactive, so we use edges to get notified about updates // connectionLookup is not reactive, so we use edges to get notified about updates
// eslint-disable-next-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
// $edges;
store.edges; store.edges;
if (onconnect || ondisconnect) { if (onconnect || ondisconnect) {
let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`); let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`);
@@ -50,7 +50,7 @@
z: zIndex = 0, z: zIndex = 0,
positionAbsolute: { x: positionX, y: positionY } positionAbsolute: { x: positionX, y: positionY }
} }
} = $derived(node); } = $derived(store.defaultNodeOptions ? { ...store.defaultNodeOptions, ...node } : node);
let { id } = node; let { id } = node;
@@ -169,7 +169,7 @@
use:drag={{ use:drag={{
nodeId: id, nodeId: id,
isSelectable: selectable, isSelectable: selectable,
disabled: false, disabled: !draggable,
handleSelector: dragHandle, handleSelector: dragHandle,
noDragClass: 'nodrag', noDragClass: 'nodrag',
nodeClickDistance, nodeClickDistance,
@@ -1,14 +0,0 @@
<script lang="ts">
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection';
const store = useStore();
</script>
<Selection
isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
width={store.selectionRect?.width}
height={store.selectionRect?.height}
x={store.selectionRect?.x}
y={store.selectionRect?.y}
/>
@@ -1 +0,0 @@
export { default as UserSelection } from './UserSelection.svelte';
@@ -11,9 +11,9 @@
}; };
} }
export function toggleSelected<Item extends Node | Edge>(ids: string[]) { export function toggleSelected<Item extends Node | Edge>(ids: Set<string>) {
return (item: Item) => { return (item: Item) => {
const isSelected = ids.includes(item.id); const isSelected = ids.has(item.id);
if (item.selected !== isSelected) { if (item.selected !== isSelected) {
return { ...item, selected: isSelected }; return { ...item, selected: isSelected };
@@ -22,17 +22,27 @@
return item; return item;
}; };
} }
// TODO: maybe replace with set.intersection?
function setEq(a: Set<string>, b: Set<string>) {
if (a.size !== b.size) {
return false;
}
for (const item of a) {
if (!b.has(item)) {
return false;
}
}
return true;
}
</script> </script>
<script lang="ts"> <script lang="ts">
import { import { SelectionMode, getEventPosition, getNodesInside } from '@xyflow/system';
SelectionMode,
getEventPosition,
getNodesInside,
getConnectedEdges
} from '@xyflow/system';
import type { Node, Edge, InternalNode } from '$lib/types'; import type { Node, Edge } from '$lib/types';
import type { PaneProps } from './types'; import type { PaneProps } from './types';
let { let {
@@ -47,7 +57,10 @@
// svelte-ignore non_reactive_update // svelte-ignore non_reactive_update
let container: HTMLDivElement; let container: HTMLDivElement;
let containerBounds: DOMRect | null = null; let containerBounds: DOMRect | null = null;
let selectedNodes: InternalNode[] = []; // let selectedNodes: InternalNode[] = [];
let selectedNodeIds: Set<string> = new Set();
let selectedEdgeIds: Set<string> = new Set();
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag); let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let isSelecting = $derived( let isSelecting = $derived(
@@ -109,7 +122,7 @@
if (!isSelecting || !containerBounds || !store.selectionRect) { if (!isSelecting || !containerBounds || !store.selectionRect) {
return; return;
} }
const start = performance.now();
selectionInProgress = true; selectionInProgress = true;
const mousePos = getEventPosition(event, containerBounds); const mousePos = getEventPosition(event, containerBounds);
@@ -122,36 +135,48 @@
width: Math.abs(mousePos.x - startX), width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY) height: Math.abs(mousePos.y - startY)
}; };
const prevSelectedNodeIds = selectedNodes.map((n) => n.id);
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, store.edges).map((e) => e.id);
selectedNodes = getNodesInside( const prevSelectedNodeIds = selectedNodeIds;
store.nodeLookup, const prevSelectedEdgeIds = selectedEdgeIds;
nextUserSelectRect,
[store.viewport.x, store.viewport.y, store.viewport.zoom], selectedNodeIds = new Set(
store.selectionMode === SelectionMode.Partial, getNodesInside(
true store.nodeLookup,
nextUserSelectRect,
[store.viewport.x, store.viewport.y, store.viewport.zoom],
store.selectionMode === SelectionMode.Partial,
true,
store.defaultNodeOptions.selectable
).map((n) => n.id)
); );
const selectedEdgeIds = getConnectedEdges(selectedNodes, store.edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id); // TODO: replace with extended connectionLookup
let edgesSelectable = store.defaultEdgeOptions.selectable ?? true;
selectedEdgeIds = new Set();
store.edges.forEach((edge) => {
if (
selectedNodeIds.has(edge.source) &&
selectedNodeIds.has(edge.target) &&
(edge.selectable ?? edgesSelectable)
) {
selectedEdgeIds.add(edge.id);
}
});
// this prevents unnecessary updates while updating the selection rectangle // this prevents unnecessary updates while updating the selection rectangle
if ( if (setEq(prevSelectedNodeIds, selectedNodeIds)) {
prevSelectedNodeIds.length !== selectedNodeIds.length ||
selectedNodeIds.some((id) => !prevSelectedNodeIds.includes(id))
) {
store.nodes = store.nodes.map(toggleSelected(selectedNodeIds)); store.nodes = store.nodes.map(toggleSelected(selectedNodeIds));
} }
if ( if (setEq(prevSelectedEdgeIds, selectedEdgeIds)) {
prevSelectedEdgeIds.length !== selectedEdgeIds.length ||
selectedEdgeIds.some((id) => !prevSelectedEdgeIds.includes(id))
) {
store.edges = store.edges.map(toggleSelected(selectedEdgeIds)); store.edges = store.edges.map(toggleSelected(selectedEdgeIds));
} }
store.selectionRectMode = 'user'; store.selectionRectMode = 'user';
store.selectionRect = nextUserSelectRect; store.selectionRect = nextUserSelectRect;
const end = performance.now();
// console.log('onPointerMove', end - start);
} }
function onPointerUp(event: PointerEvent) { function onPointerUp(event: PointerEvent) {
@@ -168,7 +193,7 @@
} }
store.selectionRect = null; store.selectionRect = null;
if (selectedNodes.length > 0) { if (selectedNodeIds.size > 0) {
store.selectionRectMode = 'nodes'; store.selectionRectMode = 'nodes';
} }
@@ -9,8 +9,8 @@
import { Viewport as ViewportComponent } from '$lib/container/Viewport'; import { Viewport as ViewportComponent } from '$lib/container/Viewport';
import { NodeRenderer } from '$lib/container/NodeRenderer'; import { NodeRenderer } from '$lib/container/NodeRenderer';
import { EdgeRenderer } from '$lib/container/EdgeRenderer'; import { EdgeRenderer } from '$lib/container/EdgeRenderer';
import { UserSelection } from '$lib/components/UserSelection';
import { NodeSelection } from '$lib/components/NodeSelection'; import { NodeSelection } from '$lib/components/NodeSelection';
import { Selection } from '$lib/components/Selection';
import { KeyHandler } from '$lib/components/KeyHandler'; import { KeyHandler } from '$lib/components/KeyHandler';
import { ConnectionLine } from '$lib/components/ConnectionLine'; import { ConnectionLine } from '$lib/components/ConnectionLine';
import { Attribution } from '$lib/components/Attribution'; import { Attribution } from '$lib/components/Attribution';
@@ -194,7 +194,13 @@
{onnodedragstop} {onnodedragstop}
/> />
</ViewportComponent> </ViewportComponent>
<UserSelection /> <Selection
isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
width={store.selectionRect?.width}
height={store.selectionRect?.height}
x={store.selectionRect?.x}
y={store.selectionRect?.y}
/>
</Pane> </Pane>
</Zoom> </Zoom>
<Attribution {proOptions} position={attributionPosition} /> <Attribution {proOptions} position={attributionPosition} />
+1
View File
@@ -40,6 +40,7 @@ export type NodeTypes = Record<
> >
>; >;
// TODO: we should be more selective about this but otherwise good to go
export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>; export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>;
export type BuiltInNode = export type BuiltInNode =
+1 -1
View File
@@ -36,5 +36,5 @@ export type HandleProps = {
/** Id of the handle /** Id of the handle
* @remarks optional if there is only one handle of this type * @remarks optional if there is only one handle of this type
*/ */
id?: string; id?: string | null;
}; };
+3 -2
View File
@@ -202,7 +202,8 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
[tx, ty, tScale]: Transform = [0, 0, 1], [tx, ty, tScale]: Transform = [0, 0, 1],
partially = false, partially = false,
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes = false excludeNonSelectableNodes = false,
defaultNodeSelectable: boolean = true
): InternalNodeBase<NodeType>[] => { ): InternalNodeBase<NodeType>[] => {
const paneRect = { const paneRect = {
...pointToRendererPoint(rect, [tx, ty, tScale]), ...pointToRendererPoint(rect, [tx, ty, tScale]),
@@ -213,7 +214,7 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
const visibleNodes: InternalNodeBase<NodeType>[] = []; const visibleNodes: InternalNodeBase<NodeType>[] = [];
for (const node of nodes.values()) { for (const node of nodes.values()) {
const { measured, selectable = true, hidden = false } = node; const { measured, selectable = defaultNodeSelectable, hidden = false } = node;
if ((excludeNonSelectableNodes && !selectable) || hidden) { if ((excludeNonSelectableNodes && !selectable) || hidden) {
continue; continue;