UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

727 lines (690 loc) 35 kB
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Pipes</title> <meta name="description" content="An editor for snapping pipes together and moving and rotating them as a single assembly." /> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Copyright 1998-2020 by Northwoods Software Corporation. --> <script src="../release/go.js"></script> <script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework --> <script id="code"> function init() { if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this var $ = go.GraphObject.make; // for more concise visual tree definitions myDiagram = $(go.Diagram, "myDiagramDiv", { initialScale: 1.5, "commandHandler.defaultScale": 1.5, allowLink: false, // no user-drawn links // use a custom DraggingTool instead of the standard one, defined below draggingTool: new SnappingTool(), contextMenu: $("ContextMenu", makeButton("Paste", function(e, obj) { e.diagram.commandHandler.pasteSelection(e.diagram.toolManager.contextMenuTool.mouseDownPoint); }, function(o) { return o.diagram.commandHandler.canPasteSelection(o.diagram.toolManager.contextMenuTool.mouseDownPoint); }), makeButton("Undo", function(e, obj) { e.diagram.commandHandler.undo(); }, function(o) { return o.diagram.commandHandler.canUndo(); }), makeButton("Redo", function(e, obj) { e.diagram.commandHandler.redo(); }, function(o) { return o.diagram.commandHandler.canRedo(); }) ), "undoManager.isEnabled": true }); // To simplify this code we define a function for creating a context menu button: function makeButton(text, action, visiblePredicate) { return $("ContextMenuButton", $(go.TextBlock, text), { click: action }, // don't bother with binding GraphObject.visible if there's no predicate visiblePredicate ? new go.Binding("visible", "", function(o, e) { return o.diagram ? visiblePredicate(o, e) : false; }).ofObject() : {}); } // when the document is modified, add a "*" to the title and enable the "Save" button myDiagram.addDiagramListener("Modified", function(e) { var button = document.getElementById("SaveButton"); if (button) button.disabled = !myDiagram.isModified; var idx = document.title.indexOf("*"); if (myDiagram.isModified) { if (idx < 0) document.title += "*"; } else { if (idx >= 0) document.title = document.title.substr(0, idx); } }); myDiagram.nodeTemplateMap.add("Comment", $(go.Node, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), $(go.TextBlock, { stroke: "brown", font: "9pt sans-serif" }, new go.Binding("text")) )); // Define the generic "pipe" Node. // The Shape gets it Geometry from a geometry path string in the bound data. // This node also gets all of its ports from an array of port data in the bound data. myDiagram.nodeTemplate = $(go.Node, "Spot", { locationObjectName: "SHAPE", locationSpot: go.Spot.Center, selectionAdorned: false, // use a Binding on the Shape.stroke to show selection itemTemplate: // each port is an "X" shape whose alignment spot and port ID are given by the item data $(go.Panel, new go.Binding("portId", "id"), new go.Binding("alignment", "spot", go.Spot.parse), $(go.Shape, "XLine", { width: 6, height: 6, background: "transparent", fill: null, stroke: "gray" }, new go.Binding("figure", "id", portFigure), // portFigure converter is defined below new go.Binding("angle", "angle")) ), // hide a port when it is connected linkConnected: function(node, link, port) { if (link.category === "") port.visible = false; }, linkDisconnected: function(node, link, port) { if (link.category === "") port.visible = true; } }, // this creates the variable number of ports for this Spot Panel, based on the data new go.Binding("itemArray", "ports"), // remember the location of this Node new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), // remember the angle of this Node new go.Binding("angle", "angle").makeTwoWay(), // move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(), $(go.Shape, { name: "SHAPE", // the following are default values; // actual values may come from the node data object via data binding geometryString: "F1 M0 0 L20 0 20 20 0 20 z", fill: "rgba(128, 128, 128, 0.5)" }, // this determines the actual shape of the Shape new go.Binding("geometryString", "geo"), // selection causes the stroke to be blue instead of black new go.Binding("stroke", "isSelected", function(s) { return s ? "dodgerblue" : "black"; }).ofObject()) ); // Show different kinds of port fittings by using different shapes in this Binding converter function portFigure(pid) { if (pid === null || pid === "") return "XLine"; if (pid[0] === 'F') return "CircleLine"; if (pid[0] === 'M') return "PlusLine"; return "XLine"; // including when the first character is 'U' } myDiagram.nodeTemplate.contextMenu = $("ContextMenu", makeButton("Rotate +45", function(e, obj) { rotate(obj.part.adornedPart, 45); }), makeButton("Rotate -45", function(e, obj) { rotate(obj.part.adornedPart, -45); }), makeButton("Rotate 180", function(e, obj) { rotate(obj.part.adornedPart, 180); }), makeButton("Detach", function(e, obj) { detachSelection(); }), makeButton("Cut", function(e, obj) { e.diagram.commandHandler.cutSelection(); }, function(o) { return o.diagram.commandHandler.canCutSelection(); }), makeButton("Copy", function(e, obj) { e.diagram.commandHandler.copySelection(); }, function(o) { return o.diagram.commandHandler.canCopySelection(); }), makeButton("Paste", function(e, obj) { e.diagram.commandHandler.pasteSelection(e.diagram.toolManager.contextMenuTool.mouseDownPoint); }, function(o) { return o.diagram.commandHandler.canPasteSelection(o.diagram.toolManager.contextMenuTool.mouseDownPoint); }), makeButton("Delete", function(e, obj) { e.diagram.commandHandler.deleteSelection(); }, function(o) { return o.diagram.commandHandler.canDeleteSelection(); }), makeButton("Undo", function(e, obj) { e.diagram.commandHandler.undo(); }, function(o) { return o.diagram.commandHandler.canUndo(); }), makeButton("Redo", function(e, obj) { e.diagram.commandHandler.redo(); }, function(o) { return o.diagram.commandHandler.canRedo(); }) ); // Change the angle of the parts connected with the given node function rotate(node, angle) { var tool = myDiagram.toolManager.draggingTool; // should be a SnappingTool myDiagram.startTransaction("rotate " + angle.toString()); var sel = new go.Set(/*go.Node*/); sel.add(node); var coll = tool.computeEffectiveCollection(sel).toKeySet(); var bounds = myDiagram.computePartsBounds(coll); var center = bounds.center; coll.each(function(n) { n.angle += angle; n.location = n.location.copy().subtract(center).rotate(angle).add(center); }); myDiagram.commitTransaction("rotate " + angle.toString()); } function detachSelection() { myDiagram.startTransaction("detach"); var coll = new go.Set(/*go.Link*/); myDiagram.selection.each(function(node) { if (!(node instanceof go.Node)) return; node.linksConnected.each(function(link) { if (link.category !== "") return; // ignore comments // ignore links to other selected nodes if (link.getOtherNode(node).isSelected) return; // disconnect this link coll.add(link); }); }); myDiagram.removeParts(coll, false); myDiagram.commitTransaction("detach"); } // no visual representation of any link data myDiagram.linkTemplate = $(go.Link, { visible: false }); // support optional links from comment nodes to pipe nodes myDiagram.linkTemplateMap.add("Comment", $(go.Link, { curve: go.Link.Bezier }, $(go.Shape, { stroke: "brown", strokeWidth: 2 }), $(go.Shape, { toArrow: "OpenTriangle", stroke: "brown" }) )); // this model needs to know about particular ports myDiagram.model = $(go.GraphLinksModel, { copiesArrays: true, copiesArrayObjects: true, linkFromPortIdProperty: "fid", linkToPortIdProperty: "tid" }); // Make sure the pipes are ordered by their key in the palette inventory function keyCompare(a, b) { var at = a.data.key; var bt = b.data.key; if (at < bt) return -1; if (at > bt) return 1; return 0; } myPalette = $(go.Palette, "myPaletteDiv", { initialScale: 1.2, contentAlignment: go.Spot.Center, nodeTemplate: myDiagram.nodeTemplate, // shared with the main Diagram "contextMenuTool.isEnabled": false, layout: $(go.GridLayout, { cellSize: new go.Size(1, 1), spacing: new go.Size(5, 5), wrappingColumn: 12, comparer: keyCompare }), // initialize the Palette with a few "pipe" nodes model: $(go.GraphLinksModel, { copiesArrays: true, copiesArrayObjects: true, linkFromPortIdProperty: "fid", linkToPortIdProperty: "tid", nodeDataArray: [ // Several different kinds of pipe objects, some already rotated for convenience. // Each "glue point" is implemented by a port. // The port's identifier's first letter must be the type of connector or "fitting". // The port's identifier's second letter must be indicate the direction in which a // connection may be made: 0-7, indicating multiples of 45-degree angles starting at zero. // If you want more than one port of a particular type in the same direction, // you will need to add a suffix to the port identifier. // The Spot determines the approximate location of the port on the shape. // The exact position is offset in order to account for the thickness of the stroke. // Each should be shifted towards the center of the shape by the fraction of its // distance from the center times the stroke thickness. // The following offsets assume the strokeWidth == 1. { key: 1, geo: "F1 M0 0 L20 0 20 20 0 20z", ports: [ { id: "U6", spot: "0.5 0 0 0.5" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 3, angle: 90, geo: "F1 M0 0 L20 0 20 20 0 20z", ports: [ { id: "U6", spot: "0.5 0 0 0.5" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 5, geo: "F1 M0 0 L20 0 20 60 0 60z", ports: [ { id: "U6", spot: "0.5 0 0 0.5" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 7, angle: 90, geo: "F1 M0 0 L20 0 20 60 0 60z", ports: [ { id: "U6", spot: "0.5 0 0 0.5" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 11, geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U2", spot: "0.25 1 0.25 -0.5" } ] }, { key: 12, angle: 90, geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U2", spot: "0.25 1 0.25 -0.5" } ] }, { key: 13, angle: 180, geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U2", spot: "0.25 1 0.25 -0.5" } ] }, { key: 14, angle: 270, geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U2", spot: "0.25 1 0.25 -0.5" } ] }, { key: 21, geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U4", spot: "0 0.25 0.5 0.25" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 22, angle: 90, geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U4", spot: "0 0.25 0.5 0.25" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 23, angle: 180, geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U4", spot: "0 0.25 0.5 0.25" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 24, angle: 270, geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z", ports: [ { id: "U0", spot: "1 0.25 -0.5 0.25" }, { id: "U4", spot: "0 0.25 0.5 0.25" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 31, geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z", ports: [ { id: "U6", spot: "0 0 10.5 0.5" }, { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 } ] }, { key: 32, angle: 90, geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z", ports: [ { id: "U6", spot: "0 0 10.5 0.5" }, { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 } ] }, { key: 33, angle: 180, geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z", ports: [ { id: "U6", spot: "0 0 10.5 0.5" }, { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 } ] }, { key: 34, angle: 270, geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z", ports: [ { id: "U6", spot: "0 0 10.5 0.5" }, { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 } ] }, { key: 41, geo: "F1 M14.142 0 L28.284 14.142 14.142 28.284 0 14.142z", ports: [ { id: "U1", spot: "1 1 -7.321 -7.321" }, { id: "U3", spot: "0 1 7.321 -7.321" }, { id: "U5", spot: "0 0 7.321 7.321" }, { id: "U7", spot: "1 0 -7.321 7.321" } ] }, // Example M-F connector pipes /* { key: 107, //angle: 90, geo: "F1 M0 0 L5 0, 5 10, 15 10, 15 0, 20 0, 20 40, 0 40z", ports: [ { id: "F6", spot: "0.5 0 0 10.5" }, { id: "U2", spot: "0.5 1 0 -0.5" } ] }, { key: 108, //angle: 90, geo: "F1 M0 0, 20 0, 20 30, 15 30, 15 40, 5 40, 5 30, 0 30z", ports: [ { id: "U6", spot: "0.5 0 0 10.5" }, { id: "M2", spot: "0.5 1 0 -0.5" } ] } */ ] // end nodeDataArray }) // end model }); // end Palette load(); } // end init // Define a custom DraggingTool function SnappingTool() { go.DraggingTool.call(this); } go.Diagram.inherit(SnappingTool, go.DraggingTool); // This predicate checks to see if the ports can snap together. // The first letter of the port id should be "U", "F", or "M" to indicate which kinds of port may connect. // The second letter of the port id should be a digit to indicate which direction it may connect. // The ports also need to not already have any link connections and need to face opposite directions. SnappingTool.prototype.compatiblePorts = function(p1, p2) { // already connected? var part1 = p1.part; var id1 = p1.portId; if (id1 === null || id1 === "") return false; if (part1.findLinksConnected(id1).filter(function(l) { return l.category === ""; }).count > 0) return false; var part2 = p2.part; var id2 = p2.portId; if (id2 === null || id2 === "") return false; if (part2.findLinksConnected(id2).filter(function(l) { return l.category === ""; }).count > 0) return false; // compatible fittings? if ((id1[0] === 'U' && id2[0] === 'U') || (id1[0] === 'F' && id2[0] === 'M') || (id1[0] === 'M' && id2[0] === 'F')) { // find their effective sides, after rotation var a1 = this.effectiveAngle(id1, part1.angle); var a2 = this.effectiveAngle(id2, part2.angle); // are they in opposite directions? if (a1 - a2 === 180 || a1 - a2 === -180) return true; } return false; }; // At what angle can a port connect, adjusting for the node's rotation SnappingTool.prototype.effectiveAngle = function(id, angle) { var dir = id[1]; var a = 0; if (dir === '1') a = 45; else if (dir === '2') a = 90; else if (dir === '3') a = 135; else if (dir === '4') a = 180; else if (dir === '5') a = 225; else if (dir === '6') a = 270; else if (dir === '7') a = 315; a += angle; if (a < 0) a += 360; else if (a >= 360) a -= 360; return a; }; // Override this method to find the offset such that a moving port can // be snapped to be coincident with a compatible stationary port, // then move all of the parts by that offset. SnappingTool.prototype.moveParts = function(parts, offset, check) { // when moving an actually copied collection of Parts, use the offset that was calculated during the drag if (this._snapOffset && this.isActive && this.diagram.lastInput.up && parts === this.copiedParts) { go.DraggingTool.prototype.moveParts.call(this, parts, this._snapOffset, check); this._snapOffset = undefined; return; } var commonOffset = offset; // find out if any snapping is desired for any Node being dragged var sit = parts.iterator; while (sit.next()) { var node = sit.key; if (!(node instanceof go.Node)) continue; var info = sit.value; var newloc = info.point.copy().add(offset); // now calculate snap point for this Node var snapoffset = newloc.copy().subtract(node.location); var nearbyports = null; var closestDistance = 20 * 20; // don't bother taking sqrt var closestPort = null; var closestPortPt = null; var nodePort = null; var mit = node.ports; while (mit.next()) { var port = mit.value; if (node.findLinksConnected(port.portId).filter(function(l) { return l.category === ""; }).count > 0) continue; var portPt = port.getDocumentPoint(go.Spot.Center); portPt.add(snapoffset); // where it would be without snapping if (nearbyports === null) { // this collects the Nodes that intersect with the NODE's bounds, // excluding nodes that are being dragged (i.e. in the PARTS collection) var nearbyparts = this.diagram.findObjectsIn(node.actualBounds, function(x) { return x.part; }, function(p) { return !parts.has(p); }, true); // gather a collection of GraphObjects that are stationary "ports" for this NODE nearbyports = new go.Set(/*go.GraphObject*/); nearbyparts.each(function(n) { if (n instanceof go.Node) { nearbyports.addAll(n.ports); } }); } var pit = nearbyports.iterator; while (pit.next()) { var p = pit.value; if (!this.compatiblePorts(port, p)) continue; var ppt = p.getDocumentPoint(go.Spot.Center); var d = ppt.distanceSquaredPoint(portPt); if (d < closestDistance) { closestDistance = d; closestPort = p; closestPortPt = ppt; nodePort = port; } } } // found something to snap to! if (closestPort !== null) { // move the node so that the compatible ports coincide var noderelpt = nodePort.getDocumentPoint(go.Spot.Center).subtract(node.location); var snappt = closestPortPt.copy().subtract(noderelpt); // save the offset, to ensure everything moves together commonOffset = snappt.subtract(newloc).add(offset); // ignore any node.dragComputation function // ignore any node.minLocation and node.maxLocation break; } } // now do the standard movement with the single (perhaps snapped) offset this._snapOffset = commonOffset.copy(); // remember for mouse-up when copying go.DraggingTool.prototype.moveParts.call(this, parts, commonOffset, check); }; // Establish links between snapped ports, // and remove obsolete links because their ports are no longer coincident. SnappingTool.prototype.doDropOnto = function(pt, obj) { go.DraggingTool.prototype.doDropOnto.call(this, pt, obj); var tool = this; // Need to iterate over all of the dropped nodes to see which ports happen to be snapped to stationary ports var coll = this.copiedParts || this.draggedParts; var it = coll.iterator; while (it.next()) { var node = it.key; if (!(node instanceof go.Node)) continue; // connect all snapped ports of this NODE (yes, there might be more than one) with links var pit = node.ports; while (pit.next()) { var port = pit.value; // maybe add a link -- see if the port is at another port that is compatible var portPt = port.getDocumentPoint(go.Spot.Center); if (!portPt.isReal()) continue; var nearbyports = this.diagram.findObjectsAt(portPt, function(x) { // some GraphObject at portPt var o = x; // walk up the chain of panels while (o !== null && o.portId === null) o = o.panel; return o; }, function(p) { // a "port" Panel // the parent Node must not be in the dragged collection, and // this port P must be compatible with the NODE's PORT if (coll.has(p.part)) return false; var ppt = p.getDocumentPoint(go.Spot.Center); if (portPt.distanceSquaredPoint(ppt) >= 0.25) return false; return tool.compatiblePorts(port, p); }); // did we find a compatible port? var np = nearbyports.first(); if (np !== null) { // connect the NODE's PORT with the other port found at the same point this.diagram.toolManager.linkingTool.insertLink(node, port, np.part, np); } } } }; // Just move selected nodes when SHIFT moving, causing nodes to be unsnapped. // When SHIFTing, must disconnect all links that connect with nodes not being dragged. // Without SHIFT, move all nodes that are snapped to selected nodes, even indirectly. SnappingTool.prototype.computeEffectiveCollection = function(parts) { if (this.diagram.lastInput.shift) { var links = new go.Set(/*go.Link*/); var coll = go.DraggingTool.prototype.computeEffectiveCollection.call(this, parts); coll.iteratorKeys.each(function(node) { // disconnect all links of this node that connect with stationary node if (!(node instanceof go.Node)) return; node.findLinksConnected().each(function(link) { if (link.category !== "") return; // see if this link connects with a node that is being dragged var othernode = link.getOtherNode(node); if (othernode !== null && !coll.has(othernode)) { links.add(link); // remember for later deletion } }); }); // outside of nested loops we can actually delete the links links.each(function(l) { l.diagram.remove(l); }); return coll; } else { var map = new go.Map(/*go.Part, Object*/); if (parts === null) return map; var tool = this; parts.iterator.each(function(n) { tool.gatherConnecteds(map, n); }); return map; } }; // Find other attached nodes. SnappingTool.prototype.gatherConnecteds = function(map, node) { if (!(node instanceof go.Node)) return; if (map.has(node)) return; // record the original Node location, for relative positioning and for cancellation map.add(node, new go.DraggingInfo(node.location)); // now recursively collect all connected Nodes and the Links to them var tool = this; node.findLinksConnected().each(function(link) { if (link.category !== "") return; // ignore comment links map.add(link, new go.DraggingInfo()); tool.gatherConnecteds(map, link.getOtherNode(node)); }); }; // end SnappingTool class function save() { document.getElementById("mySavedModel").value = myDiagram.model.toJson(); myDiagram.isModified = false; } function load() { myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value); } </script> </head> <body onload="init()"> <div id="sample"> <div id="myPaletteDiv" style="border: solid 1px black; width: 100%; height: 160px"></div> <div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 500px; margin-top: 3px"></div> <p> Nodes in this sample use <a>Shape.geometryString</a> to determine their shape. You can see more custom geometry examples and read about geometryString on the <a href="../intro/geometry.html">Geometry Path Strings Introduction page.</a> </p> <p> As a part's unconnected port (shown by an X) comes close to a stationary port with which it is compatible, the dragged selection snaps so that those ports coincide. A custom <a>DraggingTool</a>, called <b>SnappingTool</b>, is used to check compatibility. </p> <p> Dragging automatically drags all connected parts. Hold down the Shift key before dragging in order to detach a part from the parts it is connected with. These functionalities are also controlled by the custom SnappingTool. </p> <p> Use the <a>GraphObject.contextMenu</a> to rotate, detach, or delete a node. If it is connected with other parts, the whole collection rotates. </p> <div> <div> <button id="SaveButton" onclick="save()">Save</button> <button onclick="load()">Load</button> Diagram Model saved in JSON format: </div> <textarea id="mySavedModel" style="width:100%;height:300px"> { "class": "go.GraphLinksModel", "copiesArrays": true, "copiesArrayObjects": true, "linkFromPortIdProperty": "fid", "linkToPortIdProperty": "tid", "nodeDataArray": [ {"key":0, "category":"Comment", "text":"Use Shift to disconnect a shape", "loc":"0 -13"}, {"key":1, "category":"Comment", "text":"The Context Menu has more commands", "loc":"0 20"}, {"key":2, "category":"Comment", "text":"Gray Xs are unconnected ports", "loc":"0 -47"}, {"key":3, "category":"Comment", "text":"Dragged shapes snap to unconnected ports", "loc":"0 -80"}, {"key":11, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-187.33333333333331 -69.33333333333331", "angle":0}, {"key":12, "angle":90, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-147.33333333333331 -69.33333333333331"}, {"key":21, "geo":"F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U4", "spot":"0 0.25 0.5 0.25"},{"id":"U2", "spot":"0.5 1 0 -0.5"} ], "loc":"-137.33333333333331 -9.333333333333314", "angle":0}, {"key":5, "geo":"F1 M0 0 L20 0 20 60 0 60z", "ports":[ {"id":"U6", "spot":"0.5 0 0 0.5"},{"id":"U2", "spot":"0.5 1 0 -0.5"} ], "loc":"-197.33333333333331 -19.333333333333314", "angle":0}, {"key":13, "angle":180, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-147.33333333333331 30.666666666666685"}, {"key":14, "angle":270, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-187.33333333333331 30.666666666666685"}, {"key":-7, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-76.66666666666663 -8.666666666666657", "angle":0}, {"key":-8, "angle":90, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-36.66666666666663 -8.666666666666657"}, {"key":-9, "angle":180, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-36.66666666666663 31.333333333333343"}, {"key":-10, "angle":270, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-76.66666666666663 31.333333333333343"} ], "linkDataArray": [ {"from":12, "to":11, "fid":"U2", "tid":"U0"}, {"from":5, "to":11, "fid":"U6", "tid":"U2"}, {"from":13, "to":21, "fid":"U2", "tid":"U2"}, {"from":14, "to":5, "fid":"U0", "tid":"U2"}, {"from":13, "to":14, "fid":"U0", "tid":"U2"}, {"from":-8, "to":-7, "fid":"U2", "tid":"U0"}, {"from":-9, "to":-8, "fid":"U2", "tid":"U0"}, {"from":-10, "to":-7, "fid":"U0", "tid":"U2"}, {"from":-10, "to":-9, "fid":"U2", "tid":"U0"} ]} </textarea> </div> </div> </body> </html>