create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
1,078 lines (954 loc) • 38.7 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<meta name="description" content="Belts & gears: chains, pulleys, tensioners, conveyor belts, rollers." />
<meta itemprop="description" content="Belts & gears: chains, pulleys, tensioners, conveyor belts, rollers." />
<meta property="og:description" content="Belts & gears: chains, pulleys, tensioners, conveyor belts, rollers." />
<meta name="twitter:description" content="Belts & gears: chains, pulleys, tensioners, conveyor belts, rollers." />
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
<meta itemprop="name" content="Belts and Rollers Diagram Showing Continuous Belts Winding Around Circular Rollers" />
<meta property="og:title" content="Belts and Rollers Diagram Showing Continuous Belts Winding Around Circular Rollers" />
<meta name="twitter:title" content="Belts and Rollers Diagram Showing Continuous Belts Winding Around Circular Rollers" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/belts.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/belts.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/belts.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/belts.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/belts.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Belts and Rollers Diagram Showing Continuous Belts Winding Around Circular Rollers | GoJS Diagramming Library
</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
<div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
<a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
href="../">
GoJS
</a>
<div class="relative">
<button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
<svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="topnavList" class="hidden md:block">
<div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
<a href="../learn/">Learn</a>
<a href="../samples/">Samples</a>
<a href="../intro/">Intro</a>
<a href="../api/">API</a>
<a href="../download.html">Download</a>
<a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
<a id="tc" href="https://nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
<a id="tb" href="https://nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
</div>
</div>
</div>
</div>
</nav>
<script>
window.addEventListener("DOMContentLoaded", function () {
// topnav
var topButton = document.getElementById("topnavButton");
var topnavList = document.getElementById("topnavList");
if (topButton && topnavList) {
topButton.addEventListener("click", function (e) {
topnavList
.classList
.toggle("hidden");
e.stopPropagation();
});
document.addEventListener("click", function (e) {
// if the clicked element isn't the list, close the list
if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
topButton.click();
}
});
// set active <a> element
var url = window
.location
.href
.toLowerCase();
var aTags = topnavList.getElementsByTagName('a');
for (var i = 0; i < aTags.length; i++) {
var lowerhref = aTags[i]
.href
.toLowerCase();
if (lowerhref.endsWith('.html'))
lowerhref = lowerhref.slice(0, -5);
if (url.startsWith(lowerhref)) {
aTags[i]
.classList
.add('active');
break;
}
}
}
});
</script>
<div class="flex flex-col prose">
<div class="w-full max-w-screen-xl mx-auto">
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>
<div id="allSampleContent" class="p-4 w-full">
<style>
#myDiagramDiv {
width: 900px;
height: 800px;
background-color: #262626;
}
code {
font-family: monospace;
background-color: #d3d3d3;
padding: 0 3px;
}
</style>
<script id="code">
// =================== GLOBAL VARIABLES ===================
const ROLLER_DEFAULT_DIAMETER = 30;
const BELT_STROKE_WIDTH = 4;
const DEFAULT_COLOR = '#d3d3d3'; // default roller & belt color
const BACKGROUND_COLOR = '#262626';
// defined in init()
let myDiagram;
let rollersLists = {};
// =================== TEMPLATES ===================
const rollerTemplate = new go.Node('Auto', {
locationSpot: go.Spot.Center,
selectionAdorned: false,
dragComputation: rollerDragComputation
})
.add(
// roller outline circle
new go.Shape('Circle', {
name: 'GUIDE',
stroke: DEFAULT_COLOR,
strokeWidth: 2,
fill: 'transparent',
width: ROLLER_DEFAULT_DIAMETER,
height: ROLLER_DEFAULT_DIAMETER
})
.bind('stroke', 'color')
.bind('width', 'diameter')
.bind('height', 'diameter')
.bindObject('fill', '', (obj) => {
const beltKeys = getNodesBeltKeys(obj.part);
if (beltKeys.length > 1) {
const color = obj.part.data.color || DEFAULT_COLOR;
return go.Brush.mix(color, BACKGROUND_COLOR, 0.5);
} else return 'transparent';
}),
// center dot, visible if roller is attached to a belt
new go.Shape('Circle', {
width: 5,
fill: 'white',
stroke: null
})
.bind('fill', 'color')
.bind('width', 'diameter', (d) => {
if (d > 15)
return 5; // if diameter is less than 15, make the center dot smaller
else return d / 4;
})
.bindObject('visible', '', (obj) => {
const beltKeys = getNodesBeltKeys(obj.part);
if (beltKeys.length === 0) {
return false;
} else return true;
})
)
.bindTwoWay('location', 'xy', go.Point.parse, go.Point.stringify);
const beltTemplate = new go.Node({
selectionAdorned: false,
layerName: 'Foreground',
movable: false
}).add(
new go.Shape({
name: 'BELT',
fill: null,
stroke: DEFAULT_COLOR,
strokeWidth: BELT_STROKE_WIDTH,
// to create the dotted lines
// 8 px of line, 4 px of space, repeat
strokeDashArray: [8, 4]
}).bind('stroke', 'color')
);
const modelTemplate = [
{ key: '11', category: 'Roller', xy: '450 450' },
{
key: '12',
category: 'Roller',
xy: '350 600'
},
{ key: '13', category: 'Roller', xy: '300 800', diameter: 40 },
{ key: '14', category: 'Roller', xy: '150 650' },
{
key: '15',
category: 'Roller',
xy: '0 550',
color: '#daab2d'
},
{
key: '16',
category: 'Roller',
xy: '130 500'
},
{ key: '17', category: 'Roller', xy: '300 430', diameter: 100 },
{ key: '01', category: 'Roller', xy: '300 326', diameter: 100 },
{
key: 'B1',
category: 'Belt',
// the guides list defines the rollers that can interact with the belt
// the rollers the belt bends around
guides: [
{ k: '11' },
{ k: '12', outside: true, touchingBelt: true },
{ k: '13' },
{ k: '14', outside: true, touchingBelt: true },
{ k: '15' },
{ k: '16', outside: true, touchingBelt: true },
{ k: '17' }
]
},
{
key: '21',
category: 'Roller',
xy: '-100 700',
color: '#daab2d'
},
{
key: '22',
category: 'Roller',
xy: '100 800',
color: '#daab2d'
},
{
key: 'B2',
category: 'Belt',
color: '#daab2d',
guides: [{ k: '21' }, { k: '15' }, { k: '22' }, { k: '12', outside: true }]
}
];
// =================== MATH FUNCTIONS ===================
// imagine a square with corners at from and to
// returns true if pt is inside the square
function isBetween(pt, from, to) {
const bounds = {
x: Math.min(from.x, to.x),
y: Math.min(from.y, to.y),
width: Math.abs(to.x - from.x),
height: Math.abs(to.y - from.y)
};
if (
pt.x >= bounds.x &&
pt.x <= bounds.x + bounds.width &&
pt.y >= bounds.y &&
pt.y <= bounds.y + bounds.height
) {
return true;
} else {
return false;
}
}
// only used for clampDistance()
// returns a point that is dist away from pt1 towards pt2
// dist is the distance to move towards pt2, not the distance between pt1 and pt2
function partwayBetween(pt1, pt2, dist) {
return new go.Point(pt1.x - vector.x * dist, pt1.y - vector.y * dist);
}
function circleTouchingLine(pt1, pt2, center, r) {
const dist = go.Point.distanceLineSegmentSquared(
center.x,
center.y,
pt1.x,
pt1.y,
pt2.x,
pt2.y
);
if (dist < r * r) {
return true;
} else {
return false;
}
}
function segmentFullyCrossesCircle(roller, line) {
const dx = line[1].x - line[0].x;
const dy = line[1].y - line[0].y;
const r = roller.radius; // radius of the roller
const A = dx * dx + dy * dy; // A = dx^2 + dy^2
const fx = line[0].x - roller.center.x; // fx = x1 - cx
const fy = line[0].y - roller.center.y; // fy = y1 - cy
const B = 2 * (fx * dx + fy * dy); // B = 2 * (fx * dx + fy * dy)
const C = fx * fx + fy * fy - r * r; // C = fx^2 + fy^2 - r^2
const discriminant = B * B - 4 * A * C; // discriminant
if (discriminant < 0) {
// no intersection
return false;
} else {
// there is an intersection
const t1 = (-B + Math.sqrt(discriminant)) / (2 * A);
const t2 = (-B - Math.sqrt(discriminant)) / (2 * A);
// check if intersection points are between the line segment points
return t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1 && Math.abs(t1 - t2) > 0.01; // check if the intersection points are different enough
}
}
function getFromTo(guideA, guideB) {
const locA = guideA.center;
const locB = guideB.center;
const { x: x1, y: y1 } = locA;
const { x: x2, y: y2 } = locB;
const r1 = guideA.radius;
const r2 = guideB.radius;
const vector = new go.Point(x2 - x1, y2 - y1);
// angle in radians from A to B center
let centerAngle = Math.atan2(vector.y, vector.x);
// dist between A and B center
let hypotenuse = Math.hypot(vector.x, vector.y);
const opposite = guideA.outside === guideB.outside ? r2 - r1 : r2 + r1;
// sine of the angle between the center-to-center line segment and the tangent line
// imagine a right triangle with the center-to-center line segment as the hypotenuse
let offsetSine = opposite / hypotenuse; // sine = opposite / hypotenuse
// clamp offsetSine to [-1, 1] to avoid NaN in Math.asin
// in case of floating point errors
if (offsetSine < -1) offsetSine = -1;
else if (offsetSine > 1) offsetSine = 1;
// angle between the center-to-center line segment and the tangent line
// tangent offset
let offset = Math.asin(offsetSine);
let a;
if (guideB.outside) {
a = guideA.outside
? Math.PI / 2 - offset - centerAngle
: Math.PI / 2 - Math.abs(offset) - centerAngle;
} else {
a = guideA.outside
? Math.abs(offset) - Math.PI / 2 - centerAngle
: Math.PI / 2 + offset - centerAngle;
}
const cosa = Math.cos(a);
const sina = Math.sin(a);
const from =
guideB.outside && guideA.outside
? new go.Point(x1 - r1 * cosa, y1 + r1 * sina)
: new go.Point(x1 + r1 * cosa, y1 - r1 * sina);
const to =
!guideB.outside && !guideA.outside
? new go.Point(x2 + r2 * cosa, y2 - r2 * sina)
: new go.Point(x2 - r2 * cosa, y2 + r2 * sina);
return {
from: from,
to: to
};
}
// uses q1 to q2 as anchor line segment
// returns true if p1 and p2 are on opposite sides of the line segment q
// credit for concept: https://stackoverflow.com/a/3842240
function segmentsIntersect(segment1, segment2) {
function oppositeSide(p1, p2, anchorLine) {
// negative = on right side of the line segment
// positive = on left side of the line segment
// zero = on the line segment
const side1 = p1.compareWithLineSegmentPoint(anchorLine[0], anchorLine[1]);
const side2 = p2.compareWithLineSegmentPoint(anchorLine[0], anchorLine[1]);
if (side1 > 0 !== side2 > 0) {
// if one is on the right side and the other is on the left side
return true;
} else {
// otherwise, they do not intersect or only the endpoints touch
return false;
}
}
if (
oppositeSide(segment1[0], segment1[1], segment2) &&
oppositeSide(segment2[0], segment2[1], segment1)
) {
return true;
} else {
return false;
}
}
function areRollersOverlapping(node, newloc) {
let nodeRadius = node.findObject('GUIDE').actualBounds.width / 2;
let overlapping = false;
let it = node.diagram.nodes.iterator;
myDiagram.nodes.each((n) => {
if (n === node || n.category === 'Belt') return;
let dist2 = newloc.distanceSquaredPoint(n.location);
let radius = n.findObject('GUIDE').actualBounds.width / 2;
if (dist2 < (nodeRadius + radius) ** 2) {
// they are overlapping - now check if they share a belt
const nBeltKeys = getNodesBeltKeys(n);
const nodeBeltKeys = getNodesBeltKeys(node);
if (nBeltKeys.some((key) => nodeBeltKeys.includes(key))) {
overlapping = true;
}
}
});
return overlapping;
}
// =================== GET FUNCTIONS ===================
// only used in getAllRollers()
// returns data for a roller by index: center point, radius, and extra flags
function getGuideData(guides, i) {
// guide has the data about how the roller interacts with the BELT:
// if it's outside/inside, if it's touching the belt
const guide = guides[i];
const guideKey = guide.k;
// guideData is the data about the roller ITSELF:
// location, diameter
const guideData = myDiagram.model.findNodeDataForKey(guideKey);
if (guideData && guideData.xy) {
const xy = guideData.xy.split(' ').map((str) => parseInt(str));
return {
center: new go.Point(xy[0], xy[1]),
// adding half of beltWidth to the radius is is how the belt isn't directly on top of the rollers but rather looped around them
radius:
(guideData.diameter || ROLLER_DEFAULT_DIAMETER) / 2 + BELT_STROKE_WIDTH / 2,
outside: guide.outside,
touchingBelt: guide.touchingBelt,
key: guideKey
};
} else {
console.warn(`Roller with key ${guideKey} not found. (or it has no xy)`);
return null;
}
}
// used only in getOrderedRollers()
// get a list of roller data from the beltData (which has the rollers' keys)
function getAllRollers(beltData) {
const rollers = [];
for (let i = 0; i < beltData.guides.length; i++) {
rollers.push(getGuideData(beltData.guides, i));
}
return rollers;
}
// used only for orderRollers()
function getOrderedRollers(beltKey) {
const beltData = myDiagram.model.findNodeDataForKey(beltKey);
const rollers = getAllRollers(beltData);
let ordered = rollers.filter((roller) => !roller.outside || roller.touchingBelt);
let unordered = rollers.filter((roller) => roller.outside && !roller.touchingBelt);
ordered = computeContacts(ordered.slice() /* copy array */);
ordered = deleteRollersNotTouching(ordered, beltKey).ordered;
const obj = considerAddingUnorderedRollersBack(ordered, unordered);
// all items in rollers that are not in ordered
unordered = [];
for (const roller of rollers) {
if (!ordered.some((r) => r.key === roller.key)) {
unordered.push(roller);
}
}
return {
ordered: ordered,
unordered: unordered
};
}
function getAllBeltKeys() {
return myDiagram.model.nodeDataArray
.filter((n) => n.category === 'Belt')
.map((n) => n.key);
}
function getNodesBeltKeys(node) {
let beltKeys = [];
for (let beltKey in rollersLists) {
const hasNode = (key) =>
rollersLists[beltKey][key].some((roller) => roller.key === node.key);
if (hasNode('ordered') || hasNode('unordered')) {
beltKeys.push(beltKey);
}
}
return beltKeys;
}
// =================== HELPFUL FUNCTIONS ===================
// returns a point that is dist away from pt1 towards pt2
// dist is the distance to move towards pt2, not the distance between pt1 and pt2
function partwayBetween(pt1, pt2, dist) {
const vector = new go.Point(pt1.x - pt2.x, pt1.y - pt2.y);
vector.normalize();
return new go.Point(pt1.x - vector.x * dist, pt1.y - vector.y * dist);
}
// for preventing rollers from jumping around when dragged
function clampDistance(newloc, oldloc) {
// so rollers can't jump through each other
const dist = ROLLER_DEFAULT_DIAMETER;
const tooFarApart = newloc.distanceSquaredPoint(oldloc) > dist * dist;
if (tooFarApart) {
// make it move less far
newloc = partwayBetween(oldloc, newloc, dist);
}
return newloc;
}
// used to set the initial order of rollers when the belt is created
// either when program is just starting or you load new data
function orderRollers(beltKeys) {
for (const beltKey of beltKeys || getAllBeltKeys()) {
rollersLists[beltKey] = getOrderedRollers(beltKey);
}
}
function drawAllInitialBelts(beltKeys) {
for (const beltKey of beltKeys || getAllBeltKeys()) {
const rollers = recalcRollerLists(beltKey).ordered;
beltGeom(rollers, beltKey);
}
}
function updateRollerLocations(rollers, guides, node, newLoc) {
// updates the rollers' locations based on the guides
for (let i = 0; i < rollers.length; i++) {
if (node && rollers[i].key === node.key) {
// if the roller is being dragged, update its location
rollers[i].center = newLoc;
} else {
const centerString = myDiagram.model.findNodeDataForKey(rollers[i].key).xy;
rollers[i].center = go.Point.parse(centerString);
}
}
return rollers;
}
// =================== MAIN FUNCTIONS ===================
// returns a new rollers list that has .to and .from on each roller
function computeContacts(rollers) {
for (let i = 0; i < rollers.length; i++) {
const guideA = rollers[i];
// get next guide, wrap around to 0 if needed
const nexti = (i + 1) % rollers.length;
const guideB = rollers[nexti];
const { from, to } = getFromTo(guideA, guideB);
rollers[i].from = from;
rollers[nexti].to = to;
}
return rollers;
}
// prevents belt looping around roller for no reason
// cause sometimes the belt can loop around a roller that is very close to the next roller
// and in that case the next roller is not actually touching the belt
// it will usually have the to and from points very close to each other so we check for that
function weirdGlitchWillHappen(roller, rollers) {
const isVeryShortSegment = (p1, p2) =>
Math.abs(p1.x - p2.x) < 3 && Math.abs(p1.y - p2.y) < 3;
const isTouchingOtherRoller = (roller1, roller2) => {
const dist = roller1.center.distanceSquaredPoint(roller2.center);
const hypotheticalDist = (roller1.radius + roller2.radius) ** 2;
if (dist < hypotheticalDist + hypotheticalDist * 0.05) {
return true;
}
};
if (isVeryShortSegment(roller.to, roller.from)) {
// have to check that it's close enough to another roller's for that glitch to happen
for (const r of rollers) {
if (r === roller) {
continue;
}
if (isTouchingOtherRoller(r, roller)) {
return true;
}
}
}
}
// skip any guides that should not contact the Belt, because they cannot touch the path
function deleteRollersNotTouching(rollers, beltKey) {
function deleteRollers(outsideOnly) {
let deletedRollers = [];
// outside rollers
let i = 0;
while (rollers.length > 2 && i < rollers.length) {
const guide = rollers[i];
const next = rollers[(i + 1) % rollers.length];
const follow = rollers[(i + 2) % rollers.length];
if (weirdGlitchWillHappen(next, rollers)) {
deletedRollers.push(rollers.splice((i + 1) % rollers.length, 1)[0]);
break;
}
if ((next.outside || false) !== outsideOnly) {
i++;
continue;
}
const { from, to } = getFromTo(guide, follow);
let between = isBetween(next.from, from, to);
let wrongside = false;
if (between) {
// check if the next roller is on the wrong side of the line segment
wrongside = next.from.compareWithLineSegmentPoint(from, to) < 0;
if (next.outside) {
wrongside = !wrongside;
}
}
if (wrongside) {
// get rid of NEXT
deletedRollers.push(rollers.splice((i + 1) % rollers.length, 1)[0]);
// now also need to check whether GUIDE has become on the wrong side!
if (i > 0) i--;
} else {
i++;
}
}
return deletedRollers;
}
let deletedRollers = [];
deletedRollers.push(...deleteRollers(true)); // outside rollers
deletedRollers.push(...deleteRollers(false)); // inside rollers
for (const roller of deletedRollers) {
roller.touchingBelt = false; // also update the roller object
}
return {
ordered: rollers,
unordered: deletedRollers
};
}
function beltGeom(rollers, beltKey) {
// construct the Geometry for the belt Shape
const geo = new go.Geometry();
let fig = null;
for (let i = 0; i < rollers.length; i++) {
const guide = rollers[i];
// next guide (loops back to zero if current guide is the last one)
const nexti = (i + 1) % rollers.length;
const next = rollers[nexti];
if (fig === null) {
// PathFigure is a piece of a Geometry. This one starts from guide.from and this function will add PathSegments to complete it
fig = new go.PathFigure(guide.from.x, guide.from.y);
geo.add(fig);
const beltNode = myDiagram.findNodeForKey(beltKey);
beltNode.location = guide.from;
}
// lines between
fig.add(new go.PathSegment(go.SegmentType.Line, next.to.x, next.to.y));
// lines around the rollers
// next.center, next.to, and next.from are Points.
// directionPoint is a method of Points that returns the angle, in degrees, of the line from this Point to the given point.
let startang = next.center.directionPoint(next.to);
let endang = next.center.directionPoint(next.from);
let sweep = endang > startang ? endang - startang : 360 - startang + endang;
if (next.outside) {
// go counter-clockwise
fig.add(
new go.PathSegment(
go.SegmentType.Arc,
startang,
sweep - 360,
next.center.x,
next.center.y,
next.radius,
next.radius
)
);
} else {
// positive sweep angle
fig.add(
new go.PathSegment(
go.SegmentType.Arc,
startang,
sweep,
next.center.x,
next.center.y,
next.radius,
next.radius
)
);
}
}
// add the geometry to the belt Shape
const beltNode = myDiagram.findNodeForKey(beltKey);
if (beltNode) {
const beltShape = beltNode.findObject('BELT');
if (beltShape) {
let pos = geo.normalize();
beltShape.geometry = geo;
// account for the thickness of the belt shape's stroke
beltNode.position = new go.Point(
-pos.x - BELT_STROKE_WIDTH / 2,
-pos.y - BELT_STROKE_WIDTH / 2
);
}
} else {
console.warn(`Belt node with key ${beltKey} not found.`);
}
}
function considerAddingUnorderedRollersBack(ordered, unordered) {
const addBackRollers = [];
// go thru all lines & see if it's touching the line
for (let j = 0; j < unordered.length; j++) {
const roller = unordered[j];
for (let i = 0; i < ordered.length; i++) {
const before = ordered[i];
const after = ordered[(i + 1) % ordered.length];
if (circleTouchingLine(before.from, after.to, roller.center, roller.radius)) {
const obj = {
roller: roller,
i: i + 1,
unorderedI: j
};
if (roller.outside) {
obj.roller.touchingBelt = true;
}
addBackRollers.push(obj);
}
}
}
for (const info of addBackRollers) {
ordered.splice(info.i, 0, info.roller);
unordered.splice(info.unorderedI, 1);
}
return {
ordered: ordered,
unordered: unordered
};
}
function recalcRollerLists(beltKey, node, newloc) {
// same stuff as drawBelt()
const beltData = myDiagram.model.findNodeDataForKey(beltKey);
// get roller order
let rollers = rollersLists[beltKey].ordered.slice();
let unorderedRollers = rollersLists[beltKey].unordered.slice();
// have to update all roller locations based on diagram location
rollers = updateRollerLocations(rollers, beltData.guides, node, newloc);
unorderedRollers = updateRollerLocations(
unorderedRollers,
beltData.guides,
node,
newloc
);
rollers = considerAddingUnorderedRollersBack(rollers, unorderedRollers).ordered;
rollers = computeContacts(rollers);
const obj = deleteRollersNotTouching(rollers, beltKey);
rollers = obj.ordered;
unorderedRollers = [...unorderedRollers, ...obj.unordered];
rollers = computeContacts(rollers);
return {
ordered: rollers,
unordered: unorderedRollers
};
}
function areOverlappingLines(rollers) {
// make allLines list
const allLines = [];
for (let i = 0; i < rollers.length; i++) {
allLines.push([rollers[i].from, rollers[(i + 1) % rollers.length].to]);
}
const rollersToAdd = [];
// for EVERY line, check if it crosses another line
for (const line of allLines) {
for (const line2 of allLines) {
// skip if it's the same line
if (line === line2) continue;
if (segmentsIntersect(line, line2)) {
return true;
}
}
// also check if it crosses a circle
for (let i = 0; i < rollers.length; i++) {
const roller = rollers[i];
if (segmentFullyCrossesCircle(roller, line)) {
return true;
}
}
}
return false;
}
function computeUpdatedRollersLists(node, newloc, beltKeys) {
const newRollersLists = {};
for (const beltKey of beltKeys) {
newRollersLists[beltKey] = recalcRollerLists(beltKey, node, newloc);
}
return newRollersLists;
}
function moveNode(node, newRollersLists, beltKeys) {
for (beltKey of beltKeys) {
beltGeom(newRollersLists[beltKey].ordered, beltKey);
rollersLists[beltKey] = newRollersLists[beltKey];
}
}
function drawBelt(beltKey) {
const { ordered, unordered } = recalcRollerLists(beltKey);
beltGeom(ordered, beltKey);
rollersLists[beltKey].ordered = {
ordered: ordered,
unordered: unordered
};
}
function rollerDragComputation(node, newloc, snaploc) {
// don't allow rollers or drums to overlap each other
let oldloc = node.location;
// prevents rollers from jumping around when dragged where they're not supposed to be
// this is what makes moving rollers slower though
newloc = clampDistance(newloc, oldloc);
const beltKeys = getNodesBeltKeys(node);
const newRollersLists = computeUpdatedRollersLists(node, newloc, beltKeys);
if (!isNodeMoveAllowed(node, newloc, beltKeys)) {
return oldloc;
} else {
moveNode(node, newRollersLists, beltKeys);
}
myDiagram.updateAllTargetBindings();
// rounds so we only have integer locations
return new go.Point(Math.round(newloc.x), Math.round(newloc.y));
}
function isNodeMoveAllowed(node, newloc, beltKeys) {
if (areRollersOverlapping(node, newloc)) return false;
for (const beltKey of beltKeys) {
let { ordered, unordered } = recalcRollerLists(beltKey, node, newloc);
if (areOverlappingLines(ordered)) {
return false;
}
}
return true;
}
// =================== INIT ===================
function startAnimation() {
// Animate the flow in the pipes
let animation = new go.Animation();
animation.easing = go.Animation.EaseLinear;
animation.duration = 1000; // 1 second instead of default 600 milliseconds
myDiagram.nodes.each((node) => {
if (node.category !== 'Belt') return;
const belt = node.findObject('BELT');
// animate the strokeDashOffset from 36 to 0 create the illusion of a moving belt
animation.add(belt, 'strokeDashOffset', 36, 0);
});
// Run indefinitely
animation.runCount = Infinity;
animation.start();
const beltKeys = getAllBeltKeys();
orderRollers(beltKeys);
drawAllInitialBelts(beltKeys);
}
function init() {
// initialize the GoJS Diagram
myDiagram = new go.Diagram('myDiagramDiv', {
'undoManager.isEnabled': true
});
myDiagram.nodeTemplateMap.add('Roller', rollerTemplate);
myDiagram.nodeTemplateMap.add('Belt', beltTemplate);
myDiagram.model = new go.GraphLinksModel(modelTemplate);
startAnimation();
save(); // initial model appears in the textarea
}
document.addEventListener('DOMContentLoaded', init);
// =================== SAVE AND LOAD ===================
function save() {
myDiagram.startTransaction('save');
// save it in the order it currently is in
for (let beltKey in rollersLists) {
const guides = [];
for (let roller of [
...rollersLists[beltKey].ordered,
...rollersLists[beltKey].unordered
]) {
const obj = { k: roller.key };
if (roller.outside) {
obj.outside = true;
}
if (roller.touchingBelt) {
obj.touchingBelt = true;
}
guides.push(obj);
}
const beltNode = myDiagram.model.findNodeDataForKey(beltKey);
myDiagram.model.setDataProperty(beltNode, 'guides', guides);
}
document.getElementById('mySavedModel').value = myDiagram.model.toJson();
myDiagram.isModified = false;
myDiagram.commitTransaction('save');
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById('mySavedModel').value);
for (const data of myDiagram.model.nodeDataArray) {
if (!data.diameter && data.category === 'Roller') {
myDiagram.model.setDataProperty(data, 'diameter', ROLLER_DEFAULT_DIAMETER);
}
}
startAnimation();
}
</script>
<div id="sample">
<div style="display: flex">
<div id="myDiagramDiv" style="flex-grow: 1"></div>
</div>
<p>
This sample uses <a>Animation</a>s and <a>Node</a>s to give the illusion of a
continuous conveyor belt. The belt is guided by rollers and bends around them using a
custom <a>Geometry</a> built of
<code><a href="../api/symbols/PathSegment.html">PathSegments</a></code
>. Each <code>PathSegment</code> is either a line segment between two rollers or an
arc wrapping around a roller.
</p>
<p>
Rollers can be dragged around, and the belt will update automatically. The program
prevents you from moving the rollers in a way that would make:
</p>
<ul>
<li>a belt overlap itself</li>
<li>a belt go through its rollers</li>
<li>a roller overlap another roller in the same belt</li>
</ul>
<p>
Rollers can be contained to one belt or be part of multiple belts. In the model, the
<code>guides</code> list for a belt defines the rollers that can interact with the
belt. Some rollers in this sample have a fill color, meaning that they are part of
both belts shown. Try it out: you'll see that normal rollers will go right through
other belts, but rollers with a fill color will affect both belts.
</p>
<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"> </textarea>
</div>
</div>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
<div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto">
<hr/>
<h3 class="text-xl">GoJS Features in this sample</h3>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Animation</h4>
<p>
<b>GoJS</b> offers several built-in animations, enabled by default, as well as the ability to create arbitrary animations.
</p>
<p>
The <a href="../api/symbols/Diagram.html#animationManager" target="api">Diagram.animationManager</a> handles animations within a <a href="../api/symbols/Diagram.html" target="api">Diagram</a>.
The <a href="../api/symbols/AnimationManager.html" target="api">AnimationManager</a> automatically sets up and dispatches default animations, and has properties to customize and disable them.
Custom animations are possible by creating instances of <a href="../api/symbols/Animation.html" target="api">Animation</a> or <a href="../api/symbols/AnimationTrigger.html" target="api">AnimationTrigger</a>.
More information can be found in the <a href="../intro/animation.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#animation">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Geometry Path Strings</h4>
<p>
The <b>GoJS</b> <a href="../api/symbols/Geometry.html" target="api">Geometry</a> class controls the "shape" of a <a href="../api/symbols/Shape.html" target="api">Shape</a>,
whereas the <a href="../api/symbols/Shape.html#fill" target="api">Shape.fill</a> and <a href="../api/symbols/Shape.html#stroke" target="api">Shape.stroke</a> and other shape properties control the colors and appearance of the shape.
For common shape figures, there are predefined geometries that can be used by setting <a href="../api/symbols/Shape.html#figure" target="api">Shape.figure</a>.
However one can also define custom geometries.
</p>
<p>
One can construct any Geometry by allocating and initializing a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> of at least one <a href="../api/symbols/PathFigure.html" target="api">PathFigure</a> holding some <a href="../api/symbols/PathSegment.html" target="api">PathSegment</a>s.
But you may find that using the string representation of a Geometry is easier to write and save in a database.
Use the static method <a href="../api/symbols/Geometry.html#parse" target="api">Geometry.parse</a> or the <a href="../api/symbols/Shape.html#geometryString" target="api">Shape.geometryString</a> property to transform a geometry path string into a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> object.
</p>
<p>
More information can be found in the <a href="../intro/geometry.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#geometries">Related samples</a>
</p>
<hr>
</div>
</div>
</body>
<!-- This script is part of the gojs.net website, and is not needed to run the sample -->
<script src="../assets/js/goSamples.js"></script>
</html>