Browse Source

Update ftgpm-edit plugin: ports are attached to activity borders

Joeri Exelmans 4 years ago
parent
commit
d8263b36bd
1 changed files with 165 additions and 2 deletions
  1. 165 2
      src/main/webapp/plugins/cdf/ftgpm-edit.js

+ 165 - 2
src/main/webapp/plugins/cdf/ftgpm-edit.js

@@ -77,7 +77,6 @@ Draw.loadPlugin(function(ui) {
     for (const [from, to, linkType] of fromTo) {
       if (from(sourceType) && to(targetType)) {
         setStyle(linkType);
-        return true;
       }
     }
   }
@@ -119,6 +118,8 @@ Draw.loadPlugin(function(ui) {
   if (version === 'ports' || !version) {
 
     ui.editor.graph.addListener(mxEvent.CELL_CONNECTED, (_, eventObj) => {
+      // Happens whenever an edge is (dis)connected.
+
       // This will change the edge style WITHIN the transaction of the edit operation.
       // The terminal-change and style-change will be one edit operation from point of view of undo manager.
 
@@ -135,6 +136,7 @@ Draw.loadPlugin(function(ui) {
         return isDataPort(type) || isArtifact(type);
       }
 
+      // [ from, to, style ]
       const fromTo = [
         // PM control flow
         [ isControlFlowNode, isControlFlowNode, "control_flow" ],
@@ -151,8 +153,169 @@ Draw.loadPlugin(function(ui) {
       checkEdge(ui, eventObj.properties.edge, fromTo);
     });
 
+    const portClasses = [ "data_in", "data_out", "ctrl_in", "ctrl_out", ];
+    function isPort(type) {
+      return portClasses.includes(type);
+    }
+
+    // Mapping from parent shape to attached ports
+    const portMapping = new Map();
+    // Mapping from attached port to parent shape
+    const reversePortMapping = new Map();
+
+    function outsideOffset(wOrH) {
+      return 0;
+    }
+    function centerOffset(wOrH) {
+      return -wOrH / 2;
+    }
+    function insideOffset(wOrH) {
+      return -wOrH;
+    }
+
+    // Change this to position ports outside, inside or centered at the edge.
+    const borderOffset = insideOffset;
+
+    function rightOrBottomBorder(wOrH, parentWorH) {
+      return parentWorH + borderOffset(wOrH);
+    }
+    function leftOrTopBorder(wOrH) {
+      return -wOrH - borderOffset(wOrH);
+    }
+
+    function attachPortToParent(cell, moveCell) {
+      // Coordinates are relative to topleft corner of parent shape
+      const {x,y, width, height} = cell.geometry;
+      // Center of dragged shape
+      const cX = x + width/2;
+      const cY = y + height/2;
+
+      const {width: pWidth, height: pHeight} = cell.parent.geometry;
+      console.log(cX, cY, pWidth, pHeight);
+
+      // We draw two imaginary diagonals through the parent shape, to determine whether the child shape is more close to the top, right, bottom or left side.
+      //
+      //           2
+      //          /
+      //    +----+
+      //    |\ t/|
+      //    |l\/ |
+      //    | /\r|
+      //    |/b \|
+      //    +----+
+      //          \
+      //           1
+      const slope = pHeight / pWidth;
+      const above1 = cY < slope * cX;
+      const above2 = cY < pHeight - slope * cX;
+
+      let portList = portMapping.get(cell.parent);
+      if (portList === undefined) {
+        portList = [];
+        portMapping.set(cell.parent, portList);
+      }
+      // Remove cell from existing list
+      const prevParent = reversePortMapping.get(cell);
+      const prevPortList = portMapping.get(prevParent);
+      if (prevPortList) {
+        const idx = prevPortList.findIndex(({cell: c}) => c === cell);
+        if (idx >= 0) {
+          console.log("removed from list");
+          prevPortList.splice(idx, 1);
+        }        
+      }
+
+      // Because the cell was moved, a new mxGeometry object was already created for it.
+      // We simply overwrite a property of this new mxGeometry, to include the positioning of the port on the edge in the same micro-operation.
+
+      if (above1) {
+        if (above2) {
+          // console.log("top");
+          if (moveCell) {
+            cell.geometry.y = leftOrTopBorder(height);
+          }
+          portList.push({cell, side: 't'});
+        } else {
+          // console.log("right");
+          if (moveCell) {
+            cell.geometry.x = rightOrBottomBorder(width, pWidth);
+          }
+          portList.push({cell, side: 'r'});
+        }
+      } else {
+        if (above2) {
+          // console.log("left");
+          if (moveCell) {
+            cell.geometry.x = leftOrTopBorder(width);
+          }
+          portList.push({cell, side: 'l'});
+        } else {
+          // console.log("bottom");
+          if (moveCell) {
+            cell.geometry.y = rightOrBottomBorder(height, pHeight);
+          }
+          portList.push({cell, side: 'b'});
+        }
+      }
+
+      reversePortMapping.set(cell, cell.parent);
+    }
+
+    function findPortsAndAttach(cells) {
+      for (const cell of cells) {
+        if (cell.parent && cell.parent.geometry && isPort(cell.getAttribute("pmRole"))) {
+          attachPortToParent(cell, true);
+        }
+      }      
+    }
+
+    ui.editor.graph.addListener(mxEvent.MOVE_CELLS, (_, eventObj) => {
+      // console.log("MOVE_CELLS:", eventObj);
+
+      // High-level event: Happens when the user releases a dragged shape
+      findPortsAndAttach(eventObj.properties.cells);
+    })
+
+    ui.editor.graph.addListener(mxEvent.RESIZE_CELLS, (_, eventObj) => {
+      console.log("RESIZE");
+      console.log("EVENT:", eventObj);
+
+      for (const parent of eventObj.properties.cells) {
+        if (!parent.geometry) continue;
+        const portList = portMapping.get(parent);
+        if (portList !== undefined) {
+          for (const {cell, side} of portList) {
+            // We cannot just overwrite a property of the existing mxGeometry, because this would not trigger a 'move' micro-operation in the current user action. If we were to undo this edit, the port would stay in its new place. The correct implementation is to create a new mxGeometry object, and to not touch the old one, as it is used by undo/redo.
+            if (side === 'r') {
+              const newGeometry = cell.geometry.clone();
+              newGeometry.x = rightOrBottomBorder(cell.geometry.width, parent.geometry.width);
+              ui.editor.graph.model.setGeometry(cell, newGeometry);
+            } else if (side === 'b') {
+              const newGeometry = cell.geometry.clone();
+              newGeometry.y = rightOrBottomBorder(cell.geometry.height, parent.geometry.height);
+              ui.editor.graph.model.setGeometry(cell, newGeometry);
+            }
+          }
+        }
+      }
+    })
+
+    // ui.editor.undoManager.addListener(null, (_, eventObj) => {
+    //   console.log("UNDO:", eventObj);
+    // });
+
+    // Upon loading a model, 
+    ui.editor.graph.addListener(mxEvent.ROOT, (_, eventObj) => {
+      console.log("GRAPH EVENT:", eventObj);
+
+      // Root has changed (new model loaded)
+      findPortsAndAttach(Object.values(ui.editor.graph.model.cells));
+    });
+
+    window.ui = ui;
+
     ui.loadLibrary(new LocalLibrary(ui, portsExamplesLib, "FTG+PM with ports: Examples"));
-    ui.loadLibrary(new LocalLibrary(ui, portsPrimitivesLib, "FTG+PM with ports: Primitives"));    
+    ui.loadLibrary(new LocalLibrary(ui, portsPrimitivesLib, "FTG+PM with ports: Primitives"));
 
     console.log("Activated FTG+PM with ports")
   }