Adding VerticalLayout rendering component

This commit is contained in:
Jeff Avallone 2019-01-30 18:12:23 -05:00
parent 67ca83ff16
commit 91ab1dbd05
5 changed files with 245 additions and 9 deletions

23
src/quadratic-curve.js Normal file
View File

@ -0,0 +1,23 @@
class QuadraticCurve {
constructor() {
this.points = [];
this.controlPoint = { x: 0, y: 0 };
}
addPoint({ x, y }) {
const { x: cx, y: cy } = this.controlPoint;
this.points.push({ x: x - cx, y: y - cy });
if (this.points.length % 2 === 0) {
this.controlPoint = { x, y };
}
return this;
}
toString() {
return `q${ this.points.map(({ x, y }) => `${ x },${ y }`).join(' ') }`;
}
}
export default QuadraticCurve;

View File

@ -0,0 +1,157 @@
import React from 'react';
import PropTypes from 'prop-types';
import QuadraticCurve from 'quadratic-curve';
import * as style from 'rendering/style';
const radius = 10;
const VerticalLayout = ({
withConnectors,
connectorPath,
transforms,
children
}) => <>
{ withConnectors && <path style={ style.connectors } d={ connectorPath }/> }
{ React.Children.map(children, (child, i) => (
<g transform={ transforms[i] }>{ child }</g>
)) }
</>;
VerticalLayout.defaultProps = {
withConnectors: false,
spacing: 10
};
VerticalLayout.propTypes = {
spacing: PropTypes.number,
withConnectors: PropTypes.bool,
connectorPath: PropTypes.string,
transforms: PropTypes.arrayOf(PropTypes.string),
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
const generateOffsetBoxes = (boxes, spacing, containerWidth) => {
let y = 0;
return boxes.map(box => {
try {
const x = (containerWidth - box.width) / 2;
return {
...box,
x, y,
axisX1: x + box.axisX1,
axisX2: x + box.axisX2,
axisY: y + box.axisY
};
}
finally {
y += spacing + box.height;
}
});
};
const calculateChildTransforms = boxes =>
boxes.map(box => `translate(${ box.x } ${ box.y })`);
const linear = (start, end, amount) => start * (1 - amount) + end * amount;
const generateCurve = (start, end) => {
const curve = new QuadraticCurve();
const width = Math.abs(start.x - end.x);
const height = Math.abs(start.y - end.y);
const direction = {
x: start.x < end.x ? 1 : -1,
y: start.y < end.y ? 1 : -1
};
const compression = Math.max(0, 1 - height / (2 * radius));
const curveEnd = linear(radius * 2, width, compression);
curve.addPoint({
x: linear(radius, width * 0.25, compression) * direction.x,
y: 0
});
if (compression === 0) {
curve.addPoint({
x: radius * direction.x,
y: radius * direction.y
});
}
curve.addPoint({
x: linear(radius, width * 0.5, compression) * direction.x,
y: height / 2 * direction.y
});
if (compression === 0) {
curve.addPoint({
x: radius * direction.x,
y: (height - radius) * direction.y
});
}
curve
.addPoint({
x: linear(radius, width * 0.75, compression) * direction.x,
y: height * direction.y
})
.addPoint({
x: curveEnd * direction.x,
y: height * direction.y
});
if (width > curveEnd) {
curve
.addPoint({
x: (width + curveEnd) / 2 * direction.x,
y: height * direction.y
})
.addPoint({
x: width * direction.x,
y: height * direction.y
});
}
return `M ${ start.x },${ start.y } ${ curve.toString() }`;
};
const calculateConnectorPath = (boxes, { width, height }) => {
const connectorY = height / 2;
const start1 = { x: 0, y: connectorY };
const start2 = { x: width, y: connectorY };
return boxes.map(box => `
${ generateCurve(start1, { x: box.axisX1, y: box.axisY }) }
${ generateCurve(start2, { x: box.axisX2, y: box.axisY }) }
`).join('');
};
const layout = data => {
const { withConnectors, spacing } = {
...VerticalLayout.defaultProps,
...data.props
};
const childBoxes = data.children.map(child => child.box);
const curveAllowance = withConnectors ? radius * 2 : 0;
data.box = {
width: Math.max(...(childBoxes.map(box => box.width))) +
2 * curveAllowance,
height: childBoxes.reduce((height, box) => height + box.height, 0) +
(childBoxes.length - 1) * spacing
};
const offsetBoxes = generateOffsetBoxes(childBoxes, spacing, data.box.width);
data.props = {
...data.props,
transforms: calculateChildTransforms(offsetBoxes),
connectorPath: withConnectors
? calculateConnectorPath(offsetBoxes, data.box)
: undefined
};
return data;
};
export default VerticalLayout;
export { layout };

View File

@ -3,11 +3,13 @@ import * as Pin from 'rendering/Pin';
import * as Text from 'rendering/Text'; import * as Text from 'rendering/Text';
import * as Box from 'rendering/Box'; import * as Box from 'rendering/Box';
import * as HorizontalLayout from 'rendering/HorizontalLayout'; import * as HorizontalLayout from 'rendering/HorizontalLayout';
import * as VerticalLayout from 'rendering/VerticalLayout';
export default { export default {
SVG, SVG,
Pin, Pin,
Text, Text,
Box, Box,
HorizontalLayout HorizontalLayout,
VerticalLayout
}; };

View File

@ -1,6 +1,9 @@
import Render from 'components/Render'; import Render from 'components/Render';
import layout from 'layout'; import layout from 'layout';
const type = 'JS';
const description = 'JavaScript';
const parse = expr => { const parse = expr => {
return { return {
type: 'SVG', type: 'SVG',
@ -27,17 +30,41 @@ const parse = expr => {
props: { props: {
withConnectors: true withConnectors: true
}, },
children: [
{
type: 'VerticalLayout',
props: {
withConnectors: true
},
children: [ children: [
{ {
type: 'Box', type: 'Box',
props: { props: {
theme: 'literal' theme: 'literal',
label: 'Type'
}, },
children: [ children: [
{ {
type: 'Text', type: 'Text',
children: [ children: [
'JS' type
]
}
]
},
{
type: 'Box',
props: {
theme: 'literal',
label: 'Description'
},
children: [
{
type: 'Text',
children: [
description
]
}
] ]
} }
] ]

View File

@ -1,6 +1,9 @@
import Render from 'components/Render'; import Render from 'components/Render';
import layout from 'layout'; import layout from 'layout';
const type = 'PCRE';
const description = 'Perl-compatible Regular Expression';
const parse = expr => { const parse = expr => {
return { return {
type: 'SVG', type: 'SVG',
@ -27,17 +30,41 @@ const parse = expr => {
props: { props: {
withConnectors: true withConnectors: true
}, },
children: [
{
type: 'VerticalLayout',
props: {
withConnectors: true
},
children: [ children: [
{ {
type: 'Box', type: 'Box',
props: { props: {
theme: 'literal' theme: 'literal',
label: 'Type'
}, },
children: [ children: [
{ {
type: 'Text', type: 'Text',
children: [ children: [
'PCRE' type
]
}
]
},
{
type: 'Box',
props: {
theme: 'literal',
label: 'Description'
},
children: [
{
type: 'Text',
children: [
description
]
}
] ]
} }
] ]