diff --git a/src/GraphRenderer.ts b/src/GraphRenderer.ts index 466e9c4a8..9bb905531 100644 --- a/src/GraphRenderer.ts +++ b/src/GraphRenderer.ts @@ -306,6 +306,11 @@ export class GraphRenderer { static dragSelectionHandled : any = false static dragSelectionDoubleClick :boolean = false; + //drag selection region globals + static isDraggingSelectionRegion :boolean = false; + static selectionRegionStart = {x:0, y:0}; + static selectionRegionEnd = {x:0, y:0}; + static mousePosX : ko.Observable = ko.observable(-1); static mousePosY : ko.Observable = ko.observable(-1); static legacyGraph : boolean = false; //used for marking a graph when its nodes don't have a radius set. in this case we will do some conversion @@ -640,8 +645,8 @@ export class GraphRenderer { } //select handlers - if(node !== null && event.which != 2){ - // check if shift key is down, if so, add or remove selected node to/from current selection + if(node !== null && event.which != 2 && !event.shiftKey){ + // check if shift key is down, if so, add or remove selected node to/from current selection | keycode 2 is the middle mouse button if (node !== null && event.shiftKey && !event.altKey){ GraphRenderer.dragSelectionHandled = true eagle.editSelection(Eagle.RightWindowMode.Inspector, node, Eagle.FileType.Graph); @@ -685,8 +690,27 @@ export class GraphRenderer { } } }else{ - //if node is null, the empty canvas has been clicked. clear the selection - eagle.setSelection(Eagle.RightWindowMode.Inspector, null, Eagle.FileType.Graph); + if(event.shiftKey){ + //drag selection region handler + GraphRenderer.isDraggingSelectionRegion = true + GraphRenderer.selectionRegionStart = {x:GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(),y:GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y()} + GraphRenderer.selectionRegionEnd = {x:GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(),y:GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y()} + //making the selection box visible + $('#selectionRectangle').show() + + //setting start and end region to current mouse co-ordinates + $('#selectionRectangle').css({'left':GraphRenderer.selectionRegionStart.x+'px','top':GraphRenderer.selectionRegionStart.y+'px'}) + + const containerWidth = $('#logicalGraphD3Div').width() + const containerHeight = $('#logicalGraphD3Div').height() + const selectionBottomOffset = containerHeight - GraphRenderer.selectionRegionEnd.y + const selectionRightOffset = containerWidth - GraphRenderer.selectionRegionEnd.x + $('#selectionRectangle').css({'right':selectionRightOffset+'px','bottom':selectionBottomOffset+'px'}) + }else{ + //if node is null, the empty canvas has been clicked. clear the selection + eagle.setSelection(Eagle.RightWindowMode.Inspector, null, Eagle.FileType.Graph); + + } } //this is the timeout for the double click that is used to select the children of constructs @@ -700,7 +724,7 @@ export class GraphRenderer { const mouseEvent: MouseEvent = event.originalEvent; GraphRenderer.dragCurrentPosition = {x:event.pageX,y:event.pageY} if (eagle.isDragging()){ - if (eagle.draggingNode() !== null){ + if (eagle.draggingNode() !== null && !GraphRenderer.isDraggingSelectionRegion ){ const node:Node = eagle.draggingNode() $('.node.transition').removeClass('transition') @@ -755,7 +779,24 @@ export class GraphRenderer { $('#'+parent.getId()).removeClass('transition') } - } else { + } else if(GraphRenderer.isDraggingSelectionRegion){ + GraphRenderer.selectionRegionEnd = {x:GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(), y:this.SCREEN_TO_GRAPH_POSITION_Y()} + const containerWidth = $('#logicalGraphD3Div').width() + const containerHeight = $('#logicalGraphD3Div').height() + + if(GraphRenderer.selectionRegionEnd.x>GraphRenderer.selectionRegionStart.x){ + $('#selectionRectangle').css({'left':GraphRenderer.selectionRegionStart.x+'px','right':containerWidth - GraphRenderer.selectionRegionEnd.x+'px'}) + }else{ + $('#selectionRectangle').css({'left':GraphRenderer.selectionRegionEnd.x+'px','right':containerWidth - GraphRenderer.selectionRegionStart.x+'px'}) + } + + if(GraphRenderer.selectionRegionEnd.y>GraphRenderer.selectionRegionStart.y){ + $('#selectionRectangle').css({'top':GraphRenderer.selectionRegionStart.y+'px','bottom':containerHeight - GraphRenderer.selectionRegionEnd.y+'px'}) + }else{ + $('#selectionRectangle').css({'top':GraphRenderer.selectionRegionEnd.y+'px','bottom':containerHeight - GraphRenderer.selectionRegionStart.y+'px'}) + } + + }else{ // move background eagle.globalOffsetX(eagle.globalOffsetX() + mouseEvent.movementX/eagle.globalScale()); eagle.globalOffsetY(eagle.globalOffsetY() + mouseEvent.movementY/eagle.globalScale()); @@ -770,25 +811,199 @@ export class GraphRenderer { static endDrag = (node: Node, event: any) : void => { const eagle = Eagle.getInstance(); + + // if we dragged a selection region + if (GraphRenderer.isDraggingSelectionRegion){ + const nodes: Node[] = GraphRenderer.findNodesInRegion(GraphRenderer.selectionRegionStart.x, GraphRenderer.selectionRegionEnd.x, GraphRenderer.selectionRegionStart.y, GraphRenderer.selectionRegionEnd.y); + + //checking if there was no drag distance, if so we are clicking a single object and we will toggle its seletion + if(Math.abs(GraphRenderer.selectionRegionStart.x-GraphRenderer.selectionRegionEnd.x)+Math.abs(GraphRenderer.selectionRegionStart.y - GraphRenderer.selectionRegionEnd.y)<3){ + eagle.editSelection(Eagle.RightWindowMode.Inspector, node,Eagle.FileType.Graph); + }else{ + const edges: Edge[] = GraphRenderer.findEdgesContainedByNodes(eagle.logicalGraph().getEdges(), nodes); + console.log("Found", nodes.length, "nodes and", edges.length, "edges in region"); + const objects: (Node | Edge)[] = []; + + // only add those objects which are not already selected + for (const node of nodes){ + if (!eagle.objectIsSelected(node)){ + objects.push(node); + } + } + for (const edge of edges){ + if (!eagle.objectIsSelected(edge)){ + objects.push(edge); + } + } + + objects.forEach(function(element){ + eagle.editSelection(Eagle.RightWindowMode.Hierarchy, element, Eagle.FileType.Graph ) + }) + } + + + // if (isDraggingWithAlt){ + // for (const node of nodes){ + // node.setCollapsed(false); + // } + // } + + GraphRenderer.selectionRegionStart.x = 0; + GraphRenderer.selectionRegionStart.y = 0; + GraphRenderer.selectionRegionEnd.x = 0; + GraphRenderer.selectionRegionEnd.y = 0; + + // finish selecting a region + GraphRenderer.isDraggingSelectionRegion = false; + + //hide the selection rectangle + $('#selectionRectangle').hide() + + // necessary to make uncollapsed nodes show up + eagle.logicalGraph.valueHasMutated(); + }else{ + if(node != null){ + if(!GraphRenderer.dragSelectionHandled){ + const distanceMovedX = Math.abs(GraphRenderer.dragStartPosition.x-GraphRenderer.dragCurrentPosition.x) + const distanceMovedY = Math.abs(GraphRenderer.dragStartPosition.y-GraphRenderer.dragCurrentPosition.y) + + if(distanceMovedX<5 || distanceMovedY<5){ + eagle.setSelection(null, node,Eagle.FileType.Graph) + } + } + } + + if (node != null && node.getParentKey() != null){ + const parentNode = eagle.logicalGraph().findNodeByKeyQuiet(node.getParentKey()) + } + } - //console.log("endDrag", node ? node.getName() : node) eagle.isDragging(false); - eagle.draggingNode(null); - - if(node != null){ - if(!GraphRenderer.dragSelectionHandled){ - const distanceMovedX = Math.abs(GraphRenderer.dragStartPosition.x-GraphRenderer.dragCurrentPosition.x) - const distanceMovedY = Math.abs(GraphRenderer.dragStartPosition.y-GraphRenderer.dragCurrentPosition.y) + eagle.draggingNode(null) - if(distanceMovedX<5 || distanceMovedY<5){ - eagle.setSelection(null, node,Eagle.FileType.Graph) + } + + static findNodesInRegion(left: number, right: number, top: number, bottom: number): Node[] { + const eagle = Eagle.getInstance(); + const result: Node[] = []; + const nodeData : Node[] = GraphRenderer.depthFirstTraversalOfNodes(eagle.logicalGraph(), eagle.showDataNodes()); + + // re-assign left, right, top, bottom in case selection region was not dragged in the typical NW->SE direction + const realLeft = left <= right ? left : right; + const realRight = left <= right ? right : left; + const realTop = top <= bottom ? top : bottom; + const realBottom = top <= bottom ? bottom : top; + + for (let i = nodeData.length - 1; i >= 0 ; i--){ + const node : Node = nodeData[i]; + + // use center of node as position + const centerX : number = node.getPosition().x + const centerY : number = node.getPosition().y + const nodeRadius : number = node.getRadius() + + //checking if the node is fully inside the selection box + if (centerX+-nodeRadius >= realLeft && realRight+-nodeRadius >= centerX && centerY+-nodeRadius >= realTop && realBottom+-nodeRadius >= centerY){ + result.push(node); + } + } + + return result; + } + + + + static getEdges(graph: LogicalGraph, showDataNodes: boolean): Edge[]{ + if (showDataNodes){ + return graph.getEdges(); + } else { + //return [graph.getEdges()[0]]; + const edges: Edge[] = []; + + for (const edge of graph.getEdges()){ + let srcHasConnectedInput: boolean = false; + let destHasConnectedOutput: boolean = false; + + for (const e of graph.getEdges()){ + if (e.getDestNodeKey() === edge.getSrcNodeKey()){ + srcHasConnectedInput = true; + } + if (e.getSrcNodeKey() === edge.getDestNodeKey()){ + destHasConnectedOutput = true; + } } + + const srcIsDataNode: boolean = GraphRenderer.findNodeWithKey(edge.getSrcNodeKey(), graph.getNodes()).isData(); + const destIsDataNode: boolean = GraphRenderer.findNodeWithKey(edge.getDestNodeKey(), graph.getNodes()).isData(); + //console.log("edge", edge.getId(), "srcIsDataNode", srcIsDataNode, "srcHasConnectedInput", srcHasConnectedInput, "destIsDataNode", destIsDataNode, "destHasConnectedOutput", destHasConnectedOutput); + + if (destIsDataNode){ + if (!destHasConnectedOutput){ + // draw edge as normal + edges.push(edge); + } + continue; + } + + if (srcIsDataNode){ + if (srcHasConnectedInput){ + // build a new edge + const newSrc = GraphRenderer.findInputToDataNode(graph.getEdges(), edge.getSrcNodeKey()); + edges.push(new Edge(newSrc.nodeKey, newSrc.portId, edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false)); + } else { + // draw edge as normal + edges.push(edge); + } + } + } + + return edges; + } + } + + static findEdgesContainedByNodes(edges: Edge[], nodes: Node[]): Edge[]{ + const result: Edge[] = []; + + for (const edge of edges){ + const srcKey = edge.getSrcNodeKey(); + const destKey = edge.getDestNodeKey(); + let srcFound = false; + let destFound = false; + + for (const node of nodes){ + if ((node.getKey() === srcKey) || + (node.hasInputApplication() && node.getInputApplication().getKey() === srcKey) || + (node.hasOutputApplication() && node.getOutputApplication().getKey() === srcKey)){ + srcFound = true; + } + + if ((node.getKey() === destKey) || + (node.hasInputApplication() && node.getInputApplication().getKey() === destKey) || + (node.hasOutputApplication() && node.getOutputApplication().getKey() === destKey)){ + destFound = true; + } + } + + if (srcFound && destFound){ + result.push(edge); } } - if (node != null && node.getParentKey() != null){ - const parentNode = eagle.logicalGraph().findNodeByKeyQuiet(node.getParentKey()) + return result; + } + + + static findInputToDataNode(edges: Edge[], nodeKey: number) : {nodeKey:number, portId: string}{ + for (const edge of edges){ + if (edge.getDestNodeKey() === nodeKey){ + return { + nodeKey: edge.getSrcNodeKey(), + portId: edge.getSrcPortId() + }; + } } + + return null; } static centerConstructs = (construct:Node, graphNodes:Node[]) :void => { diff --git a/static/graph.css b/static/graph.css index 3c43f26d9..40bc6d6b3 100644 --- a/static/graph.css +++ b/static/graph.css @@ -19,6 +19,12 @@ -webkit-transform-origin: 0 0; } +#selectionRectangle{ + border:1px black solid; + position: absolute; + display: none; +} + #logicalGraphD3Div svg{ position:absolute; z-index: 2; @@ -50,7 +56,6 @@ pointer-events: all; } - .node .container, .constructOutputApp, .constructInputApp{ z-index: 5; pointer-events: all; diff --git a/templates/base.html b/templates/base.html index 80db63dfa..52b4428b9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -124,6 +124,7 @@ +