refactor(onlyRenderVisible): change from nodes to elements #677

This commit is contained in:
moklick
2020-11-14 16:46:14 +01:00
parent 6e0b31d9c1
commit 2420cf3689
7 changed files with 121 additions and 98 deletions
+15 -83
View File
@@ -4,12 +4,11 @@ import { useStoreState } from '../../store/hooks';
import ConnectionLine from '../../components/ConnectionLine/index'; import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph'; import { isEdge } from '../../utils/graph';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { getHandlePosition, getHandle } from './utils';
import { import {
XYPosition,
Position, Position,
Edge, Edge,
Node, Node,
ElementId,
HandleElement, HandleElement,
Elements, Elements,
ConnectionLineType, ConnectionLineType,
@@ -33,78 +32,6 @@ interface EdgePositions {
targetY: number; targetY: number;
} }
function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
if (!handle) {
switch (position) {
case Position.Top:
return {
x: node.__rf.width / 2,
y: 0,
};
case Position.Right:
return {
x: node.__rf.width,
y: node.__rf.height / 2,
};
case Position.Bottom:
return {
x: node.__rf.width / 2,
y: node.__rf.height,
};
case Position.Left:
return {
x: 0,
y: node.__rf.height / 2,
};
}
}
switch (position) {
case Position.Top:
return {
x: handle.x + handle.width / 2,
y: handle.y,
};
case Position.Right:
return {
x: handle.x + handle.width,
y: handle.y + handle.height / 2,
};
case Position.Bottom:
return {
x: handle.x + handle.width / 2,
y: handle.y + handle.height,
};
case Position.Left:
return {
x: handle.x,
y: handle.y + handle.height / 2,
};
}
}
function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null | undefined {
let handle = null;
if (!bounds) {
return null;
}
// there is no handleId when there are no multiple handles/ handles with ids
// so we just pick the first one
if (bounds.length === 1 || !handleId) {
handle = bounds[0];
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId);
}
if (typeof handle === 'undefined') {
return null;
}
return handle;
}
function getEdgePositions( function getEdgePositions(
sourceNode: Node, sourceNode: Node,
sourceHandle: HandleElement | unknown, sourceHandle: HandleElement | unknown,
@@ -132,25 +59,29 @@ function getEdgePositions(
function renderEdge( function renderEdge(
edge: Edge, edge: Edge,
props: EdgeRendererProps, props: EdgeRendererProps,
visibleNodes: Node[],
nodes: Node[], nodes: Node[],
selectedElements: Elements | null, selectedElements: Elements | null,
elementsSelectable: boolean elementsSelectable: boolean
) { ) {
const sourceId = edge.source;
const sourceHandleId = edge.sourceHandle || null; const sourceHandleId = edge.sourceHandle || null;
const targetId = edge.target;
const targetHandleId = edge.targetHandle || null; const targetHandleId = edge.targetHandle || null;
const sourceNode = nodes.find((n) => n.id === sourceId); const sourceNode = nodes.find((n) => n.id === edge.source);
const targetNode = nodes.find((n) => n.id === targetId); const targetNode = nodes.find((n) => n.id === edge.target);
const renderEdge = visibleNodes.some((n) => n.id === edge.source || n.id == edge.target);
if (!renderEdge) {
return null;
}
if (!sourceNode) { if (!sourceNode) {
console.warn(`couldn't create edge for source id: ${sourceId}`); console.warn(`couldn't create edge for source id: ${edge.source}`);
return null; return null;
} }
if (!targetNode) { if (!targetNode) {
console.warn(`couldn't create edge for target id: ${targetId}`); console.warn(`couldn't create edge for target id: ${edge.target}`);
return null; return null;
} }
@@ -224,7 +155,6 @@ function renderEdge(
const EdgeRenderer = (props: EdgeRendererProps) => { const EdgeRenderer = (props: EdgeRendererProps) => {
const [tX, tY, tScale] = useStoreState((state) => state.transform); const [tX, tY, tScale] = useStoreState((state) => state.transform);
const edges = useStoreState((state) => state.edges); const edges = useStoreState((state) => state.edges);
const nodes = useStoreState((state) => state.nodes);
const connectionNodeId = useStoreState((state) => state.connectionNodeId); const connectionNodeId = useStoreState((state) => state.connectionNodeId);
const connectionHandleId = useStoreState((state) => state.connectionHandleId); const connectionHandleId = useStoreState((state) => state.connectionHandleId);
const connectionHandleType = useStoreState((state) => state.connectionHandleType); const connectionHandleType = useStoreState((state) => state.connectionHandleType);
@@ -234,6 +164,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
const elementsSelectable = useStoreState((state) => state.elementsSelectable); const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width); const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height); const height = useStoreState((state) => state.height);
const visibleNodes = useStoreState((state) => state.visibleNodes);
const nodes = useStoreState((state) => state.nodes);
const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props; const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props;
@@ -248,10 +180,10 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<svg width={width} height={height} className="react-flow__edges"> <svg width={width} height={height} className="react-flow__edges">
<MarkerDefinitions color={arrowHeadColor} /> <MarkerDefinitions color={arrowHeadColor} />
<g transform={transformStyle}> <g transform={transformStyle}>
{edges.map((edge: Edge) => renderEdge(edge, props, nodes, selectedElements, elementsSelectable))} {edges.map((edge: Edge) => renderEdge(edge, props, visibleNodes, nodes, selectedElements, elementsSelectable))}
{renderConnectionLine && ( {renderConnectionLine && (
<ConnectionLine <ConnectionLine
nodes={nodes} nodes={visibleNodes}
connectionNodeId={connectionNodeId!} connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId} connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!} connectionHandleType={connectionHandleType!}
+73 -1
View File
@@ -3,7 +3,7 @@ import { ComponentType } from 'react';
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'; import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge'; import wrapEdge from '../../components/Edges/wrapEdge';
import { EdgeTypesType, EdgeProps } from '../../types'; import { EdgeTypesType, EdgeProps, Position, Node, XYPosition, ElementId, HandleElement } from '../../types';
export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType { export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
const standardTypes: EdgeTypesType = { const standardTypes: EdgeTypesType = {
@@ -27,3 +27,75 @@ export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
...specialTypes, ...specialTypes,
}; };
} }
export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
if (!handle) {
switch (position) {
case Position.Top:
return {
x: node.__rf.width / 2,
y: 0,
};
case Position.Right:
return {
x: node.__rf.width,
y: node.__rf.height / 2,
};
case Position.Bottom:
return {
x: node.__rf.width / 2,
y: node.__rf.height,
};
case Position.Left:
return {
x: 0,
y: node.__rf.height / 2,
};
}
}
switch (position) {
case Position.Top:
return {
x: handle.x + handle.width / 2,
y: handle.y,
};
case Position.Right:
return {
x: handle.x + handle.width,
y: handle.y + handle.height / 2,
};
case Position.Bottom:
return {
x: handle.x + handle.width / 2,
y: handle.y + handle.height,
};
case Position.Left:
return {
x: handle.x,
y: handle.y + handle.height / 2,
};
}
}
export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null | undefined {
let handle = null;
if (!bounds) {
return null;
}
// there is no handleId when there are no multiple handles/ handles with ids
// so we just pick the first one
if (bounds.length === 1 || !handleId) {
handle = bounds[0];
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId);
}
if (typeof handle === 'undefined') {
return null;
}
return handle;
}
+1 -1
View File
@@ -19,7 +19,7 @@ interface FlowRendererProps
| 'snapGrid' | 'snapGrid'
| 'connectionLineType' | 'connectionLineType'
| 'arrowHeadColor' | 'arrowHeadColor'
| 'onlyRenderVisibleNodes' | 'onlyRenderVisibleElements'
| 'selectNodesOnDrag' | 'selectNodesOnDrag'
> { > {
children: ReactNode; children: ReactNode;
+9 -3
View File
@@ -20,7 +20,7 @@ export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange'
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
snapToGrid: boolean; snapToGrid: boolean;
snapGrid: [number, number]; snapGrid: [number, number];
onlyRenderVisibleNodes: boolean; onlyRenderVisibleElements: boolean;
defaultZoom: number; defaultZoom: number;
defaultPosition: [number, number]; defaultPosition: [number, number];
arrowHeadColor: string; arrowHeadColor: string;
@@ -58,7 +58,7 @@ const GraphView = ({
onConnectEnd, onConnectEnd,
snapToGrid, snapToGrid,
snapGrid, snapGrid,
onlyRenderVisibleNodes, onlyRenderVisibleElements,
nodesDraggable, nodesDraggable,
nodesConnectable, nodesConnectable,
elementsSelectable, elementsSelectable,
@@ -93,6 +93,7 @@ const GraphView = ({
const setMinZoom = useStoreActions((actions) => actions.setMinZoom); const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom); const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
const setOnlyRenderVisibleElements = useStoreActions((actions) => actions.setOnlyRenderVisibleElements);
const currentStore = useStore(); const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
@@ -187,6 +188,12 @@ const GraphView = ({
} }
}, [translateExtent]); }, [translateExtent]);
useEffect(() => {
if (typeof onlyRenderVisibleElements !== 'undefined') {
setOnlyRenderVisibleElements(onlyRenderVisibleElements);
}
}, [translateExtent]);
return ( return (
<FlowRenderer <FlowRenderer
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
@@ -223,7 +230,6 @@ const GraphView = ({
onNodeContextMenu={onNodeContextMenu} onNodeContextMenu={onNodeContextMenu}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
selectNodesOnDrag={selectNodesOnDrag} selectNodesOnDrag={selectNodesOnDrag}
snapToGrid={snapToGrid} snapToGrid={snapToGrid}
snapGrid={snapGrid} snapGrid={snapGrid}
+2 -7
View File
@@ -1,7 +1,6 @@
import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import React, { memo, useMemo, ComponentType, MouseEvent } from 'react';
import { useStoreState } from '../../store/hooks'; import { useStoreState } from '../../store/hooks';
import { getNodesInside } from '../../utils/graph';
import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types'; import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types';
interface NodeRendererProps { interface NodeRendererProps {
@@ -14,19 +13,17 @@ interface NodeRendererProps {
onNodeContextMenu?: (event: MouseEvent, node: Node) => void; onNodeContextMenu?: (event: MouseEvent, node: Node) => void;
onNodeDragStart?: (event: MouseEvent, node: Node) => void; onNodeDragStart?: (event: MouseEvent, node: Node) => void;
onNodeDragStop?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void;
onlyRenderVisibleNodes: boolean;
snapToGrid: boolean; snapToGrid: boolean;
snapGrid: [number, number]; snapGrid: [number, number];
} }
const NodeRenderer = (props: NodeRendererProps) => { const NodeRenderer = (props: NodeRendererProps) => {
const nodes = useStoreState((state) => state.nodes);
const transform = useStoreState((state) => state.transform); const transform = useStoreState((state) => state.transform);
const selectedElements = useStoreState((state) => state.selectedElements); const selectedElements = useStoreState((state) => state.selectedElements);
const viewportBox = useStoreState((state) => state.viewportBox);
const nodesDraggable = useStoreState((state) => state.nodesDraggable); const nodesDraggable = useStoreState((state) => state.nodesDraggable);
const nodesConnectable = useStoreState((state) => state.nodesConnectable); const nodesConnectable = useStoreState((state) => state.nodesConnectable);
const elementsSelectable = useStoreState((state) => state.elementsSelectable); const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const visibleNodes = useStoreState((state) => state.visibleNodes);
const transformStyle = useMemo( const transformStyle = useMemo(
() => ({ () => ({
@@ -35,11 +32,9 @@ const NodeRenderer = (props: NodeRendererProps) => {
[transform[0], transform[1], transform[2]] [transform[0], transform[1], transform[2]]
); );
const nodesToRender = props.onlyRenderVisibleNodes ? getNodesInside(nodes, viewportBox, transform, true) : nodes;
return ( return (
<div className="react-flow__nodes" style={transformStyle}> <div className="react-flow__nodes" style={transformStyle}>
{nodesToRender.map((node) => { {visibleNodes.map((node) => {
const nodeType = node.type || 'default'; const nodeType = node.type || 'default';
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
+3 -3
View File
@@ -88,7 +88,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
multiSelectionKeyCode?: KeyCode; multiSelectionKeyCode?: KeyCode;
snapToGrid?: boolean; snapToGrid?: boolean;
snapGrid?: [number, number]; snapGrid?: [number, number];
onlyRenderVisibleNodes?: boolean; onlyRenderVisibleElements?: boolean;
nodesDraggable?: boolean; nodesDraggable?: boolean;
nodesConnectable?: boolean; nodesConnectable?: boolean;
elementsSelectable?: boolean; elementsSelectable?: boolean;
@@ -142,7 +142,7 @@ const ReactFlow = ({
multiSelectionKeyCode = 'Meta', multiSelectionKeyCode = 'Meta',
snapToGrid = false, snapToGrid = false,
snapGrid = [15, 15], snapGrid = [15, 15],
onlyRenderVisibleNodes = true, onlyRenderVisibleElements = true,
selectNodesOnDrag = true, selectNodesOnDrag = true,
nodesDraggable, nodesDraggable,
nodesConnectable, nodesConnectable,
@@ -200,7 +200,7 @@ const ReactFlow = ({
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
snapToGrid={snapToGrid} snapToGrid={snapToGrid}
snapGrid={snapGrid} snapGrid={snapGrid}
onlyRenderVisibleNodes={onlyRenderVisibleNodes} onlyRenderVisibleElements={onlyRenderVisibleElements}
nodesDraggable={nodesDraggable} nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable} nodesConnectable={nodesConnectable}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
+18
View File
@@ -45,10 +45,12 @@ export interface StoreModel {
viewportBox: Computed<StoreModel, Rect>; viewportBox: Computed<StoreModel, Rect>;
transform: Transform; transform: Transform;
elements: Elements; elements: Elements;
visibleNodes: Computed<StoreModel, Node[]>;
nodes: Computed<StoreModel, Node[]>; nodes: Computed<StoreModel, Node[]>;
edges: Computed<StoreModel, Edge[]>; edges: Computed<StoreModel, Edge[]>;
selectedElements: Elements | null; selectedElements: Elements | null;
selectedNodesBbox: Rect; selectedNodesBbox: Rect;
onlyRenderVisibleElements: boolean;
d3Zoom: ZoomBehavior<Element, unknown> | null; d3Zoom: ZoomBehavior<Element, unknown> | null;
d3Selection: D3Selection<Element, unknown, null, undefined> | null; d3Selection: D3Selection<Element, unknown, null, undefined> | null;
@@ -131,6 +133,8 @@ export interface StoreModel {
unsetUserSelection: Action<StoreModel>; unsetUserSelection: Action<StoreModel>;
setMultiSelectionActive: Action<StoreModel, boolean>; setMultiSelectionActive: Action<StoreModel, boolean>;
setOnlyRenderVisibleElements: Action<StoreModel, boolean>;
} }
export const storeModel: StoreModel = { export const storeModel: StoreModel = {
@@ -140,9 +144,19 @@ export const storeModel: StoreModel = {
transform: [0, 0, 1], transform: [0, 0, 1],
elements: [], elements: [],
nodes: computed((state) => state.elements.filter(isNode)), nodes: computed((state) => state.elements.filter(isNode)),
visibleNodes: computed((state) => {
if (!state.onlyRenderVisibleElements) {
return state.nodes;
}
const viewportBox = { x: 0, y: 0, width: state.width, height: state.height };
return getNodesInside(state.nodes, viewportBox, state.transform, true);
}),
edges: computed((state) => state.elements.filter(isEdge)), edges: computed((state) => state.elements.filter(isEdge)),
selectedElements: null, selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
onlyRenderVisibleElements: true,
d3Zoom: null, d3Zoom: null,
d3Selection: null, d3Selection: null,
@@ -440,6 +454,10 @@ export const storeModel: StoreModel = {
setMultiSelectionActive: action((state, isActive) => { setMultiSelectionActive: action((state, isActive) => {
state.multiSelectionActive = isActive; state.multiSelectionActive = isActive;
}), }),
setOnlyRenderVisibleElements: action((state, onlyRenderVisible) => {
state.onlyRenderVisibleElements = onlyRenderVisible;
}),
}; };
const nodeEnv: string = (typeof __ENV__ !== 'undefined' && __ENV__) as string; const nodeEnv: string = (typeof __ENV__ !== 'undefined' && __ENV__) as string;