gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
727 lines (690 loc) • 35 kB
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>