diff --git a/src/rendering/Box/style.js b/src/rendering/Box/style.js
index 96c563f..6357b44 100644
--- a/src/rendering/Box/style.js
+++ b/src/rendering/Box/style.js
@@ -4,11 +4,11 @@ import {
tan,
grey,
blue,
- fontFamily,
- fontSizeSmall,
- strokeBase
+ strokeBase,
+ infoText
} from 'rendering/style';
+export { infoText };
export const literal = {
fill: blue,
strokeWidth: '1px',
@@ -31,7 +31,3 @@ export const capture = {
export const anchor = {
fill: brown
};
-export const infoText = {
- fontSize: fontSizeSmall,
- fontFamily
-};
diff --git a/src/rendering/Loop/index.js b/src/rendering/Loop/index.js
new file mode 100644
index 0000000..e11e9d3
--- /dev/null
+++ b/src/rendering/Loop/index.js
@@ -0,0 +1,133 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { getBBox } from 'layout';
+
+import * as style from 'rendering/style';
+
+const radius = 10;
+const arrowSize = 5;
+
+const Loop = ({
+ label,
+ labelTransform,
+ contentTransform,
+ loopPaths,
+ children
+}) => {
+ const labelProps = {
+ transform: labelTransform,
+ style: style.infoText
+ };
+
+ return <>
+
+ { label && { label } }
+
+ { children }
+
+ >;
+};
+
+Loop.propTypes = {
+ label: PropTypes.string,
+ greedy: PropTypes.bool,
+ skip: PropTypes.bool,
+ repeat: PropTypes.bool,
+ labelTransform: PropTypes.string,
+ contentTransform: PropTypes.string,
+ loopPaths: PropTypes.string,
+ children: PropTypes.node.isRequired
+};
+
+const generateOffsetBox = (box, { skip, repeat }) => {
+ let x = 0;
+ let y = 0;
+
+ if (skip) {
+ x = 1.5 * radius;
+ y = radius;
+ } else if (repeat) {
+ x = radius;
+ }
+
+ return {
+ ...box,
+ x, y,
+ axisX1: x + box.axisX1,
+ axisX2: x + box.axisX2,
+ axisY: y + box.axisY
+ };
+};
+const skipPath = (box, greedy) => {
+ const vert = Math.max(0, box.axisY - box.y - radius);
+ const horiz = box.width - radius;
+
+ return `
+ M 0,${ box.axisY }
+ q ${ radius },0 ${ radius },${ -radius }
+ v ${ -vert }
+ q 0,${ -radius } ${ radius },${ -radius }
+ h ${ horiz }
+ q ${ radius },0 ${ radius },${ radius }
+ v ${ vert }
+ q 0,${ radius } ${ radius },${ radius }
+ ` + (greedy ? '' : `
+ M ${ radius },${ box.axisY - 1.5 * radius }
+ l ${ arrowSize },${ arrowSize }
+ z
+ l ${ -arrowSize },${ arrowSize }
+ `);
+};
+const repeatPath = (box, greedy) => {
+ const vert = box.y + box.height - box.axisY - radius;
+
+ return `
+ M ${ box.x },${ box.axisY }
+ q ${ -radius },0 ${ -radius },${ radius }
+ v ${ vert }
+ q 0,${ radius } ${ radius },${ radius }
+ h ${ box.width }
+ q ${ radius },0 ${ radius },${ -radius }
+ v ${ -vert }
+ q 0,${ -radius } ${ -radius },${ -radius }
+ ` + (greedy ? `
+ m ${ radius },${ 1.5 * radius }
+ l ${ arrowSize },${ -arrowSize }
+ z
+ l ${ -arrowSize },${ -arrowSize }
+ ` : '');
+};
+const layout = data => {
+ const { label, greedy, skip, repeat } = data.props || {};
+ const childBox = generateOffsetBox(data.children[0].box, { skip, repeat });
+ const labelBox = label ?
+ getBBox({ label }) :
+ { width: 0, height: 0 };
+ const width = childBox.width + childBox.x * 2;
+ const height = childBox.height + labelBox.height +
+ (skip ? 10 : 0) + (repeat ? 10 : 0);
+
+ data.box = {
+ width,
+ height,
+ axisY: childBox.axisY,
+ axisX1: childBox.axisX1,
+ axisX2: childBox.axisX2
+ };
+
+ data.props = {
+ ...data.props,
+ labelTransform: `translate(${ width - labelBox.width } ${ height })`,
+ contentTransform: `translate(${ childBox.x } ${ childBox.y })`,
+ loopPaths: [
+ skip && skipPath(childBox, greedy),
+ repeat && repeatPath(childBox, greedy)
+ ].filter(Boolean).join('')
+ };
+
+ return data;
+};
+
+export default Loop;
+export { layout };
diff --git a/src/rendering/style.js b/src/rendering/style.js
index 4b5e474..85048fe 100644
--- a/src/rendering/style.js
+++ b/src/rendering/style.js
@@ -23,3 +23,8 @@ export const connectors = {
fillOpacity: 0,
...strokeBase
};
+
+export const infoText = {
+ fontSize: fontSizeSmall,
+ fontFamily
+};
diff --git a/src/rendering/types.js b/src/rendering/types.js
index 6fa6d19..9907124 100644
--- a/src/rendering/types.js
+++ b/src/rendering/types.js
@@ -2,6 +2,7 @@ import * as SVG from 'rendering/SVG';
import * as Pin from 'rendering/Pin';
import * as Text from 'rendering/Text';
import * as Box from 'rendering/Box';
+import * as Loop from 'rendering/Loop';
import * as HorizontalLayout from 'rendering/HorizontalLayout';
import * as VerticalLayout from 'rendering/VerticalLayout';
@@ -10,6 +11,7 @@ export default {
Pin,
Text,
Box,
+ Loop,
HorizontalLayout,
VerticalLayout
};