Skip to content
Snippets Groups Projects
ToolchainConnection.jsx 4.39 KiB
// @flow
import * as React from 'react';
import { ContextMenu, MenuItem, ContextMenuTrigger } from 'react-contextmenu';
import cn from 'classnames';

import type { BlockCoords, ConnectionType } from './types.js';

type Props = {
	fromLocMap: BlockCoords,
	toLocMap: BlockCoords,
	// provides the location of the ends of the connection
	connection: ConnectionType,
	// the color of the channel the connection is sync'd to
	channelColor: string,
	errors: string[],
	handleContextMenu?: (e: any, ConnectionType) => any,
};

// gets a unique id for the connection
export const connectionToId = ({ from, to, channel }: {from: string, to: string, channel: string}) =>
	`${ from.replace('.', '-') }-${ to.replace('.', '-') }`;

class ToolchainConnection extends React.Component<Props> {
	constructor(props: Props){
		super(props);
	}

	/*
	 * Custom implementation of shouldComponentUpdate for performance reasons!
	 * "Why not just use PureComponent??"
	 * PureComponent implements a default shouldComponentUpdate that shallowly compares
	 * all current & next props/state values.
	 * Because the click handlers are always not equal (since they're dynamically generated in the loop in
	 * GraphicalEditor.jsx), blocks are *always* rerendered.
	 * Also, the locMap objects are technically somehow a different object whenever
	 * a subset of the blocks are moved.
	 * This means that every block change means rerendering all the blocks.
	 * By writing a custom shouldComponentUpdate, we can use more detailed logic that prevents
	 * the unnecessary rerenders.
	 * A couple things to keep in mind:
	 * - the click handlers dont functionally change if the block data doesnt change.
	 * - the errors array is a different array every time technically, so check the values
	 * - only the positions (x & y) of the locmap data matters
	 * - the channel in the connection data can change, but not the to/from
	 */
	shouldComponentUpdate(nextProps: Props){
		for(const key in nextProps){
			if(key === 'fromLocMap' || key === 'toLocMap' || key === 'handleContextMenu'){
				continue;
			} else if(key === 'errors'){
				if(nextProps[key].length !== this.props.errors.length)
					return true;
				for(const i in nextProps[key]){
					if(nextProps[key][i] !== this.props[key][i])
						return true;
				}
			} else if(key === 'connection'){
				if(nextProps[key].channel !== this.props[key].channel
					|| nextProps[key].from !== this.props[key].from
					|| nextProps[key].to !== this.props[key].to
				)
					return true;
			} else if(nextProps[key] !== this.props[key]){
				return true;
			}
		}

		const fromLoc = this.props.fromLocMap;
		const toLoc = this.props.toLocMap;
		const fromLocNext = nextProps.fromLocMap;
		const toLocNext = nextProps.toLocMap;

		if(fromLoc.x !== fromLocNext.x)
			return true;
		if(fromLoc.y !== fromLocNext.y)
			return true;
		if(toLoc.x !== toLocNext.x)
			return true;
		if(toLoc.y !== toLocNext.y)
			return true;

		return false;
	}

	render = () => {
		const conn = this.props.connection;
		const connectionId = `#${ connectionToId(conn) }`;
		const fromLoc = this.props.fromLocMap;
		const toLoc = this.props.toLocMap;
		//console.log(`connecting "${ connId }" from ${ fromInfo } to ${ toInfo }`);
		//x & y coords for the beginning and end of the connection

		/* the right-click menu wraps the actual lines */
		return (
			<g className='tcConnection'
				onContextMenu={e => this.props.handleContextMenu ? this.props.handleContextMenu(e, conn) : {}}
			>
				{/* the first line is almost invisible to widen the space for the mouse events (so its easier for users to do stuff */}
				<line
					id={connectionId}
					stroke={this.props.channelColor}
					strokeOpacity={0.005}
					strokeWidth={10}
					x1={fromLoc.x + 2}
					y1={fromLoc.y + 9}
					x2={toLoc.x + 2}
					y2={toLoc.y + 9}
				>
					<title>
						Connection from {conn.from} to {conn.to} synchronized to the {conn.channel} channel{' '}
						{ this.props.errors[0] }
					</title>
				</line>
				{/* the second line is just for show */}
				<line
					className={cn({ error: this.props.errors.length > 0 })}
					stroke={this.props.channelColor}
					strokeWidth={2}
					x1={fromLoc.x + 2}
					y1={fromLoc.y + 6}
					x2={toLoc.x + 2}
					y2={toLoc.y + 6}
				>
					<title>
						Connection from {conn.from} to {conn.to} synchronized to the {conn.channel} channel{' '}
						{ (this.props.errors || [])[0] }
					</title>
				</line>
			</g>
		);
	}
}

export default ToolchainConnection;