gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
334 lines (310 loc) • 12.8 kB
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Belts</title>
<meta name="description" content="Belts & gears: chains, pulleys, tensioners, conveyor belts, rollers." />
<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;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"InitialLayoutCompleted": function(e) {
updateBelts(); // changes the bounds of the "Belt"s
e.diagram.alignDocument(go.Spot.Center, go.Spot.Center);
},
"animationManager.isEnabled": false,
"undoManager.isEnabled": true,
allowCopy: false,
allowDelete: false,
"draggingTool.moveParts": function(parts, offset, check) {
go.DraggingTool.prototype.moveParts.call(this, parts, offset, check);
updateBelts(); // this is inefficient if there are a lot of belts
}
});
function commonStyle() {
return [
new go.Binding("location", "xy", go.Point.parse).makeTwoWay(go.Point.stringify),
{
locationSpot: go.Spot.Center, locationObjectName: "GUIDE",
selectionAdorned: false,
dragComputation: function(node, newloc, snaploc) {
// don't allow rollers or drums to overlap each other
var oldloc = node.location;
var noderad = node.findObject("GUIDE").actualBounds.width/2;
var ok = true;
var it = node.diagram.nodes.iterator;
while (it.next()) {
var n = it.value;
if (n === node || n.category === "Belt") continue;
var dist2 = newloc.distanceSquaredPoint(n.location);
var rad = n.findObject("GUIDE").actualBounds.width/2;
if (dist2 < (noderad+rad)*(noderad+rad)) { ok = false; break; }
}
return ok ? newloc : oldloc;
}
}
];
}
myDiagram.nodeTemplateMap.add("Roller",
$(go.Node, "Spot", commonStyle(),
$(go.Shape, "Circle",
{ name: "GUIDE", fill: "lightgray", strokeWidth: 0, width: 20, height: 20 },
new go.Binding("width", "diameter").makeTwoWay(),
new go.Binding("height", "diameter")),
$(go.TextBlock,
{ font: "6pt sans-serif", stroke: "black" },
new go.Binding("text", "key"))
));
myDiagram.nodeTemplateMap.add("Drum",
$(go.Node, "Spot", commonStyle(),
$(go.Shape, "Circle",
{ name: "GUIDE", fill: "lightgray", stroke: "gray", width: 80, height: 80 },
new go.Binding("fill", "color")),
$(go.TextBlock,
{ font: "6pt sans-serif", stroke: "darkblue" },
new go.Binding("text", "key"))
));
myDiagram.nodeTemplateMap.add("Belt",
$(go.Node,
{ selectionAdorned: false, layerName: "Foreground", copyable: false, movable: false },
$(go.Shape,
{ name: "BELT", fill: null, stroke: "gray", strokeWidth: 2, strokeDashArray: [4, 2] },
new go.Binding("stroke", "color"))
));
load();
} // end init
function updateBelts(coll) {
if (!coll) coll = myDiagram.nodes;
myDiagram.startTransaction();
coll.each(updateBelt);
myDiagram.commitTransaction("updated belts");
}
function updateBelt(node) {
if (node.category !== "Belt") return;
var belt = node.findObject("BELT");
var diagram = node.diagram;
var guideinfos = node.data.guides;
if (!Array.isArray(guideinfos)) throw new Error("data.guides is not an Array for Belt node: " + node.data.key);
// gather basic information about each guide node
var guides = []; // holds Objects with handy information
for (var i = 0; i < guideinfos.length; i++) {
var info = guideinfos[i];
var guidenode = diagram.findNodeForKey(info.k);
if (guidenode !== null && guidenode.location.isReal()) {
var loc = guidenode.location;
var cyl = guidenode.findObject("GUIDE");
var radius = (cyl !== null) ? cyl.measuredBounds.width / 2 : 10;
if (guides.length > 0) {
var prevguide = guides[guides.length - 1];
var prevloc = prevguide.location;
var prevradius = prevguide.radius;
var dist = Math.sqrt(prevloc.distanceSquaredPoint(loc));
if (dist < Math.abs(prevradius-radius)) { // one is completely inside the other
if (prevradius > radius) {
continue; // skip this smaller guide
} else {
guides.pop(); // skip the previous guide, which was smaller
}
}
}
guides.push({
node: guidenode,
location: guidenode.location.copy(),
radius: radius + belt.strokeWidth/2,
outside: !!info.outside,
from: null, // these Points will be computed by computeContacts
to: null
});
}
}
// handle some degenerate cases
if (guides.length < 2) {
if (guides.length === 1) {
node.location = guides[0].location;
}
if (belt !== null) {
belt.geometry = new go.Geometry(go.Geometry.Ellipse);
}
return;
}
// compute the contact points
// assume guides are listed in clockwise order
for (var i = 0; i < guides.length; i++) {
var guide = guides[i];
var next = guides[(i + 1) % guides.length];
computeContacts(guide, next);
}
// skip any guides that should not contact the Belt, because they're cannot touch the path
var i = 0;
while (guides.length > 2 && i < guides.length) {
var guide = guides[i];
var next = guides[(i + 1) % guides.length];
var follow = guides[(i + 2) % guides.length];
// is NEXT on the wrong side of the line from GUIDE to FOLLOW?
var wrongside = comparePointWithLine(guide.from.x, guide.from.y, follow.to.x, follow.to.y, next.to.x, next.to.y) < 0;
if (next.outside) wrongside = !wrongside;
if (wrongside) {
computeContacts(guide, follow);
// get rid of NEXT
if (i + 1 < guides.length) {
guides.splice(i + 1, 1);
} else {
guides.splice(0, 1);
}
// now also need to check whether GUIDE has become on the wrong side!
if (i > 0) i--;
} else {
i++;
}
}
// construct the Geometry for the belt Shape
var geo = new go.Geometry();
var fig = null;
for (var i = 0; i < guides.length; i++) {
var guide = guides[i];
var next = guides[(i + 1) % guides.length];
if (fig === null) {
fig = new go.PathFigure(guide.from.x, guide.from.y, true);
geo.add(fig);
}
fig.add(new go.PathSegment(go.PathSegment.Line, next.to.x, next.to.y));
var startang = next.location.directionPoint(next.to);
var endang = next.location.directionPoint(next.from);
var sweep = (endang > startang) ? endang-startang : (360 - startang) + endang;
if (next.outside) { // go counter-clockwise
fig.add(new go.PathSegment(go.PathSegment.Arc, startang, sweep - 360, next.location.x, next.location.y, next.radius, next.radius));
} else { // positive sweep angle
fig.add(new go.PathSegment(go.PathSegment.Arc, startang, sweep, next.location.x, next.location.y, next.radius, next.radius));
}
}
// update the Belt's Shape.geometry
if (belt !== null) {
var pos = geo.normalize();
belt.geometry = geo;
// account for the thickness of the belt shape's stroke
node.position = new go.Point(-pos.x - belt.strokeWidth / 2, -pos.y - belt.strokeWidth / 2);
node.ensureBounds();
}
} // end updateBelt
function comparePointWithLine(a1x, a1y, a2x, a2y, p1x, p1y) {
var x2 = a2x - a1x;
var y2 = a2y - a1y;
var px = p1x - a1x;
var py = p1y - a1y;
var ccw = px * y2 - py * x2;
if (ccw === 0) {
ccw = px * x2 + py * y2;
if (ccw > 0) {
px -= x2;
py -= y2;
ccw = px * x2 + py * y2;
if (ccw < 0) ccw = 0;
}
}
return (ccw < 0) ? -1 : ((ccw > 0) ? 1 : 0);
}
function computeContacts(guideA, guideB) {
var locA = guideA.location;
var x1 = locA.x;
var y1 = locA.y;
var r1 = guideA.radius;
var locB = guideB.location;
var x2 = locB.x;
var y2 = locB.y;
var r2 = guideB.radius;
// this assumes that belts only go clockwise
var g = Math.atan2(y2 - y1, x2 - x1);
var d = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
var bb = ((guideA.outside === guideB.outside) ? (r2 - r1) : (r2 + r1)) / d;
if (bb < -1) bb = -1; else if (bb > 1) bb = 1;
var b = Math.asin(bb);
if (guideB.outside) {
if (guideA.outside) { // both outside
var a = Math.PI / 2 - b - g;
var cosa = Math.cos(a);
var sina = Math.sin(a);
guideA.from = new go.Point(x1 - r1 * cosa, y1 + r1 * sina);
guideB.to = new go.Point(x2 - r2 * cosa, y2 + r2 * sina);
} else { // inside A, outside B
var a = Math.PI / 2 - Math.abs(b) - g;
var cosa = Math.cos(a);
var sina = Math.sin(a);
guideA.from = new go.Point(x1 + r1 * cosa, y1 - r1 * sina);
guideB.to = new go.Point(x2 - r2 * cosa, y2 + r2 * sina);
}
} else {
if (guideA.outside) { // outside A, inside B
var a = Math.abs(b) - Math.PI / 2 - g;
var cosa = Math.cos(a);
var sina = Math.sin(a);
guideA.from = new go.Point(x1 + r1 * cosa, y1 - r1 * sina);
guideB.to = new go.Point(x2 - r2 * cosa, y2 + r2 * sina);
} else { // both inside
var a = Math.PI / 2 + b - g;
var cosa = Math.cos(a);
var sina = Math.sin(a);
guideA.from = new go.Point(x1 + r1 * cosa, y1 - r1 * sina);
guideB.to = new go.Point(x2 + r2 * cosa, y2 - r2 * sina);
}
}
}
// Show the diagram's model in JSON format that the user may edit
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
var beltAnimation = null;
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
// Animate the flow in the pipes
if (beltAnimation) beltAnimation.stop();
var animation = new go.Animation();
animation.easing = go.Animation.EaseLinear;
myDiagram.nodes.each(function(node) {
if (node.category !== "Belt") return;
animation.add(node.findObject("BELT"), "strokeDashOffset", 36, 0)
});
// Run indefinitely
animation.runCount = Infinity;
animation.start();
beltAnimation = animation;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
Diagram Model saved in JSON format:
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"nodeDataArray": [
{"key":"P111", "category":"Roller", "xy":"450 610"},
{"key":"P112", "category":"Roller", "xy":"400 660"},
{"key":"P113", "category":"Roller", "xy":"350 725"},
{"key":"P114", "category":"Roller", "xy":"305 800"},
{"key":"P115", "category":"Roller", "xy":"280 705"},
{"key":"P116", "category":"Roller", "xy":"200 720"},
{"key":"P117", "category":"Roller", "xy":"200 620"},
{"key":"D1", "category":"Drum", "xy":"300 540"},
{"key":"D2", "category":"Drum", "xy":"300 622"},
{"key":"B1", "category":"Belt", "color":"blue",
"guides":[ {"k":"D2"},{"k":"P111"},{"k":"P112", "outside":true},{"k":"P113", "outside":true},{"k":"P114"},{"k":"P115", "outside":true},{"k":"P116"},{"k":"P117"} ]},
{"key":"P211", "category":"Roller", "xy":"100 750"},
{"key":"P212", "category":"Roller", "xy":"150 800"},
{"key":"B2", "category":"Belt", "color":"green",
"guides":[ {"k":"P211"},{"k":"P116"},{"k":"P212"} ]}
],
"linkDataArray": []}
</textarea>
</div>
</body>
</html>