From 91ab1dbd05b6b0341f60224263bd7b3268bf4664 Mon Sep 17 00:00:00 2001 From: Jeff Avallone Date: Wed, 30 Jan 2019 18:12:23 -0500 Subject: [PATCH] Adding VerticalLayout rendering component --- src/quadratic-curve.js | 23 ++++ src/rendering/VerticalLayout/index.js | 157 ++++++++++++++++++++++++++ src/rendering/types.js | 4 +- src/syntax/js.js | 35 +++++- src/syntax/pcre.js | 35 +++++- 5 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 src/quadratic-curve.js create mode 100644 src/rendering/VerticalLayout/index.js diff --git a/src/quadratic-curve.js b/src/quadratic-curve.js new file mode 100644 index 0000000..90f368c --- /dev/null +++ b/src/quadratic-curve.js @@ -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; diff --git a/src/rendering/VerticalLayout/index.js b/src/rendering/VerticalLayout/index.js new file mode 100644 index 0000000..e4e74ee --- /dev/null +++ b/src/rendering/VerticalLayout/index.js @@ -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 && } + { React.Children.map(children, (child, i) => ( + { child } + )) } +; + +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 }; diff --git a/src/rendering/types.js b/src/rendering/types.js index 709458f..6fa6d19 100644 --- a/src/rendering/types.js +++ b/src/rendering/types.js @@ -3,11 +3,13 @@ import * as Pin from 'rendering/Pin'; import * as Text from 'rendering/Text'; import * as Box from 'rendering/Box'; import * as HorizontalLayout from 'rendering/HorizontalLayout'; +import * as VerticalLayout from 'rendering/VerticalLayout'; export default { SVG, Pin, Text, Box, - HorizontalLayout + HorizontalLayout, + VerticalLayout }; diff --git a/src/syntax/js.js b/src/syntax/js.js index c371b46..94e410c 100644 --- a/src/syntax/js.js +++ b/src/syntax/js.js @@ -1,6 +1,9 @@ import Render from 'components/Render'; import layout from 'layout'; +const type = 'JS'; +const description = 'JavaScript'; + const parse = expr => { return { type: 'SVG', @@ -29,15 +32,39 @@ const parse = expr => { }, children: [ { - type: 'Box', + type: 'VerticalLayout', props: { - theme: 'literal' + withConnectors: true }, children: [ { - type: 'Text', + type: 'Box', + props: { + theme: 'literal', + label: 'Type' + }, children: [ - 'JS' + { + type: 'Text', + children: [ + type + ] + } + ] + }, + { + type: 'Box', + props: { + theme: 'literal', + label: 'Description' + }, + children: [ + { + type: 'Text', + children: [ + description + ] + } ] } ] diff --git a/src/syntax/pcre.js b/src/syntax/pcre.js index 1ba9f6b..4bf294a 100644 --- a/src/syntax/pcre.js +++ b/src/syntax/pcre.js @@ -1,6 +1,9 @@ import Render from 'components/Render'; import layout from 'layout'; +const type = 'PCRE'; +const description = 'Perl-compatible Regular Expression'; + const parse = expr => { return { type: 'SVG', @@ -29,15 +32,39 @@ const parse = expr => { }, children: [ { - type: 'Box', + type: 'VerticalLayout', props: { - theme: 'literal' + withConnectors: true }, children: [ { - type: 'Text', + type: 'Box', + props: { + theme: 'literal', + label: 'Type' + }, children: [ - 'PCRE' + { + type: 'Text', + children: [ + type + ] + } + ] + }, + { + type: 'Box', + props: { + theme: 'literal', + label: 'Description' + }, + children: [ + { + type: 'Text', + children: [ + description + ] + } ] } ]