Refactors UI code of integrated circuits completely into separate folders and adds dragndrop functionality for the UI (#60514)

Co-authored-by: Watermelon914 <3052169-Watermelon914@users.noreply.gitlab.com>
This commit is contained in:
Watermelon914
2021-07-29 22:26:48 -07:00
committed by GitHub
co-authored by Watermelon914
parent 9f546e1f78
commit 27f3ab0d5d
9 changed files with 760 additions and 596 deletions
+38 -8
View File
@@ -27,6 +27,8 @@ export class InfinitePlane extends Component {
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleZoomIncrease = this.handleZoomIncrease.bind(this);
this.handleZoomDecrease = this.handleZoomDecrease.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.doOffsetMouse = this.doOffsetMouse.bind(this);
@@ -70,14 +72,46 @@ export class InfinitePlane extends Component {
});
}
handleZoomIncrease(event) {
const { onZoomChange } = this.props;
const { zoom } = this.state;
const newZoomValue = Math.min(zoom+ZOOM_INCREMENT, ZOOM_MAX_VAL);
this.setState({
zoom: newZoomValue,
});
if (onZoomChange) {
onZoomChange(newZoomValue);
}
}
handleZoomDecrease(event) {
const { onZoomChange } = this.props;
const { zoom } = this.state;
const newZoomValue = Math.max(zoom-ZOOM_INCREMENT, ZOOM_MIN_VAL);
this.setState({
zoom: newZoomValue,
});
if (onZoomChange) {
onZoomChange(newZoomValue);
}
}
handleMouseMove(event) {
const { onBackgroundMoved } = this.props;
if (this.state.mouseDown) {
let newX, newY;
this.setState((state) => {
newX = event.clientX - state.lastLeft;
newY = event.clientY - state.lastTop;
return {
left: event.clientX - state.lastLeft,
top: event.clientY - state.lastTop,
left: newX,
top: newY,
};
});
if (onBackgroundMoved) {
onBackgroundMoved(newX, newY);
}
}
}
@@ -140,9 +174,7 @@ export class InfinitePlane extends Component {
<Stack.Item>
<Button
icon="minus"
onClick={() => this.setState({
zoom: Math.max(zoom-ZOOM_INCREMENT, ZOOM_MIN_VAL),
})}
onClick={this.handleZoomDecrease}
/>
</Stack.Item>
<Stack.Item grow={1}>
@@ -157,9 +189,7 @@ export class InfinitePlane extends Component {
<Stack.Item>
<Button
icon="plus"
onClick={() => this.setState({
zoom: Math.min(zoom+ZOOM_INCREMENT, ZOOM_MAX_VAL),
})}
onClick={this.handleZoomIncrease}
/>
</Stack.Item>
</Stack>
@@ -0,0 +1,27 @@
import { Stack, Button } from '../../components';
export const BasicInput = (props, context) => {
const { children, name, setValue, defaultValue, value } = props;
return (
(value !== null && (
<Stack onMouseDown={(e) => e.stopPropagation()}>
<Stack.Item>
<Button
color="transparent"
compact
icon="times"
onClick={() => setValue(null, { set_null: true })}
/>
</Stack.Item>
<Stack.Item>{children}</Stack.Item>
</Stack>
)) || (
<Button
content={name}
color="transparent"
compact
onClick={() => setValue(defaultValue)}
/>
)
);
};
@@ -0,0 +1,57 @@
import { CSS_COLORS } from '../../constants';
import { SVG_CURVE_INTENSITY } from './constants';
import { classes } from '../../../common/react';
export const Connections = (props, context) => {
const { connections } = props;
const isColorClass = (str) => {
if (typeof str === 'string') {
return CSS_COLORS.includes(str);
}
};
return (
<svg
width="100%"
height="100%"
style={{
'position': 'absolute',
'pointer-events': 'none',
'z-index': -1,
}}>
{connections.map((val, index) => {
const from = val.from;
const to = val.to;
if (!to || !from) {
return;
}
// Starting point
let path = `M ${from.x} ${from.y}`;
// DEFAULT STYLE
path += `C ${from.x + SVG_CURVE_INTENSITY}, ${from.y},`;
path += `${to.x - SVG_CURVE_INTENSITY}, ${to.y},`;
path += `${to.x}, ${to.y}`;
// SUBWAY STYLE
// const yDiff = Math.abs(from.y - (to.y - 16));
// path += `L ${to.x - yDiff} ${from.y}`;
// path += `L ${to.x - 16} ${to.y}`;
// path += `L ${to.x} ${to.y}`;
val.color = val.color || 'blue';
return (
<path
className={classes([
isColorClass(val.color) && `color-stroke-${val.color}`,
])}
key={index}
d={path}
fill="transparent"
stroke-width="2px"
/>
);
})}
</svg>
);
};
@@ -0,0 +1,60 @@
import { useBackend } from '../../backend';
import {
Box,
Button, Flex,
} from '../../components';
import { FUNDAMENTAL_DATA_TYPES } from './FundamentalTypes';
import { NULL_REF } from './constants';
export const DisplayName = (props, context) => {
const { act } = useBackend(context);
const { port, isOutput, componentId, portIndex, ...rest } = props;
const InputComponent = FUNDAMENTAL_DATA_TYPES[port.type || 'any'];
const hasInput = !isOutput
&& port.connected_to === NULL_REF
&& InputComponent;
return (
<Box {...rest}>
<Flex direction="column">
<Flex.Item>
{(hasInput && (
<InputComponent
setValue={(val, extraParams) => act('set_component_input', {
component_id: componentId,
port_id: portIndex,
input: val,
...extraParams,
})}
color={port.color}
name={port.name}
value={port.current_data} />
))
|| (isOutput && (
<Button
compact
color="transparent"
onClick={() => act('get_component_value', {
component_id: componentId,
port_id: portIndex,
})}>
<Box color="white">{port.name}</Box>
</Button>
))
|| port.name}
</Flex.Item>
<Flex.Item>
<Box
fontSize={0.75}
opacity={0.25}
textAlign={isOutput ? 'right' : 'left'}>
{port.type || 'any'}
</Box>
</Flex.Item>
</Flex>
</Box>
);
};
@@ -0,0 +1,84 @@
import { BasicInput } from './BasicInput';
import { NumberInput, Button, Stack, Input } from '../../components';
export const FUNDAMENTAL_DATA_TYPES = {
'string': (props, context) => {
const { name, value, setValue, color } = props;
return (
<BasicInput name={name} setValue={setValue} value={value} defaultValue="">
<Input
placeholder={name}
value={value}
onChange={(e, val) => setValue(val)}
/>
</BasicInput>
);
},
'number': (props, context) => {
const { name, value, setValue, color } = props;
return (
<BasicInput
name={name}
setValue={setValue}
value={value}
defaultValue={0}>
<NumberInput
value={value}
color={color}
onChange={(e, val) => setValue(val)}
unit={name}
/>
</BasicInput>
);
},
'entity': (props, context) => {
const { name, setValue, color } = props;
return (
<Button
content={name}
color="transparent"
icon="upload"
compact
onClick={() => setValue(null, { marked_atom: true })}
/>
);
},
'signal': (props, context) => {
const { name, setValue } = props;
return (
<Button
content={name}
color="transparent"
compact
onClick={() => setValue()}
/>
);
},
'any': (props, context) => {
const { name, value, setValue, color } = props;
return (
<BasicInput
name={name}
setValue={setValue}
value={value}
defaultValue={''}>
<Stack>
<Stack.Item>
<Button
color={color}
icon="upload"
onClick={() => setValue(null, { marked_atom: true })}
/>
</Stack.Item>
<Stack.Item>
<Input
placeholder={name}
value={value}
onChange={(e, val) => setValue(val)}
/>
</Stack.Item>
</Stack>
</BasicInput>
);
},
};
@@ -0,0 +1,221 @@
import { useBackend } from '../../backend';
import {
Box,
Stack, Button, Dropdown,
} from '../../components';
import { Component } from 'inferno';
import { shallowDiffers } from '../../../common/react';
import { ABSOLUTE_Y_OFFSET } from './constants';
import { Port } from "./Port";
export class ObjectComponent extends Component {
constructor() {
super();
this.state = {
isDragging: false,
dragPos: null,
startPos: null,
lastMousePos: null,
};
this.handleStartDrag = this.handleStartDrag.bind(this);
this.handleStopDrag = this.handleStopDrag.bind(this);
this.handleDrag = this.handleDrag.bind(this);
}
handleStartDrag(e) {
const { x, y } = this.props;
e.stopPropagation();
this.setState({
lastMousePos: null,
isDragging: true,
dragPos: { x: x, y: y },
startPos: { x: x, y: y },
});
window.addEventListener('mousemove', this.handleDrag);
window.addEventListener('mouseup', this.handleStopDrag);
}
handleStopDrag(e) {
const { act } = useBackend(this.context);
const { dragPos } = this.state;
const { index } = this.props;
if (dragPos) {
act('set_component_coordinates', {
component_id: index,
rel_x: dragPos.x,
rel_y: dragPos.y,
});
}
window.removeEventListener('mousemove', this.handleDrag);
window.removeEventListener('mouseup', this.handleStopDrag);
this.setState({ isDragging: false });
}
handleDrag(e) {
const { dragPos, isDragging, lastMousePos } = this.state;
if (dragPos && isDragging) {
e.preventDefault();
const { screenZoomX, screenZoomY, screenX, screenY } = e;
let xPos = screenZoomX || screenX;
let yPos = screenZoomY || screenY;
if (lastMousePos) {
this.setState({
dragPos: {
x: dragPos.x - (lastMousePos.x - xPos),
y: dragPos.y - (lastMousePos.y - yPos),
},
});
}
this.setState({
lastMousePos: { x: xPos, y: yPos },
});
}
}
shouldComponentUpdate(nextProps, nextState) {
const { input_ports, output_ports } = this.props;
return (
shallowDiffers(this.props, nextProps)
|| shallowDiffers(this.state, nextState)
|| shallowDiffers(input_ports, nextProps.input_ports)
|| shallowDiffers(output_ports, nextProps.output_ports)
);
}
render() {
const {
input_ports,
output_ports,
name,
x,
y,
index,
color = 'blue',
options,
option,
removable,
locations,
onPortUpdated,
onPortLoaded,
onPortMouseDown,
onPortRightClick,
onPortMouseUp,
...rest
} = this.props;
const { act } = useBackend(this.context);
const { startPos, dragPos } = this.state;
let [x_pos, y_pos] = [x, y];
if (dragPos && startPos && startPos.x === x_pos && startPos.y === y_pos) {
x_pos = dragPos.x;
y_pos = dragPos.y;
}
// Assigned onto the ports
const PortOptions = {
onPortLoaded: onPortLoaded,
onPortUpdated: onPortUpdated,
onPortMouseDown: onPortMouseDown,
onPortRightClick: onPortRightClick,
onPortMouseUp: onPortMouseUp,
};
return (
<Box
{...rest}
position="absolute"
left={`${x_pos}px`}
top={`${y_pos}px`}
onMouseDown={this.handleStartDrag}
onMouseUp={this.handleStopDrag}
onComponentWillUnmount={this.handleDrag}>
<Box
backgroundColor={color}
py={1}
px={1}
className="ObjectComponent__Titlebar">
<Stack>
<Stack.Item grow={1} unselectable="on">
{name}
</Stack.Item>
{!!options && (
<Stack.Item>
<Dropdown
color={color}
nochevron
over
options={options}
displayText={option}
noscroll
onSelected={(selected) => act('set_component_option', {
component_id: index,
option: selected,
})} />
</Stack.Item>
)}
<Stack.Item>
<Button
color="transparent"
icon="info"
compact
onClick={(e) => act('set_examined_component', {
component_id: index,
x: e.pageX,
y: e.pageY + ABSOLUTE_Y_OFFSET,
})} />
</Stack.Item>
{!!removable && (
<Stack.Item>
<Button
color="transparent"
icon="times"
compact
onClick={() => act('detach_component', { component_id: index })} />
</Stack.Item>
)}
</Stack>
</Box>
<Box
className="ObjectComponent__Content"
unselectable="on"
py={1}
px={1}>
<Stack>
<Stack.Item grow={1}>
<Stack vertical fill>
{input_ports.map((port, portIndex) => (
<Stack.Item key={portIndex}>
<Port
port={port}
portIndex={portIndex + 1}
componentId={index}
{...PortOptions}
/>
</Stack.Item>
))}
</Stack>
</Stack.Item>
<Stack.Item ml={5}>
<Stack vertical>
{output_ports.map((port, portIndex) => (
<Stack.Item key={portIndex}>
<Port
port={port}
portIndex={portIndex + 1}
componentId={index}
{...PortOptions}
isOutput />
</Stack.Item>
))}
</Stack>
</Stack.Item>
</Stack>
</Box>
</Box>
);
}
}
@@ -0,0 +1,111 @@
import {
Stack,
Icon,
} from '../../components';
import { Component, createRef } from 'inferno';
import { DisplayName } from "./DisplayName";
export class Port extends Component {
constructor() {
super();
this.iconRef = createRef();
this.componentDidUpdate = this.componentDidUpdate.bind(this);
this.componentDidMount = this.componentDidMount.bind(this);
this.handlePortMouseDown = this.handlePortMouseDown.bind(this);
this.handlePortRightClick = this.handlePortRightClick.bind(this);
this.handlePortMouseUp = this.handlePortMouseUp.bind(this);
}
handlePortMouseDown(e) {
const {
port,
portIndex,
componentId,
isOutput,
onPortMouseDown,
} = this.props;
onPortMouseDown(portIndex, componentId, port, isOutput, e);
}
handlePortMouseUp(e) {
const {
port,
portIndex,
componentId,
isOutput,
onPortMouseUp,
} = this.props;
onPortMouseUp(portIndex, componentId, port, isOutput, e);
}
handlePortRightClick(e) {
const {
port,
portIndex,
componentId,
isOutput,
onPortRightClick,
} = this.props;
onPortRightClick(portIndex, componentId, port, isOutput, e);
}
componentDidUpdate() {
const { port, onPortUpdated } = this.props;
if (onPortUpdated) {
onPortUpdated(port, this.iconRef.current);
}
}
componentDidMount() {
const { port, onPortLoaded } = this.props;
if (onPortLoaded) {
onPortLoaded(port, this.iconRef.current);
}
}
render() {
const {
port,
portIndex,
componentId,
isOutput,
...rest
} = this.props;
return (
<Stack {...rest} justify={isOutput ? 'flex-end' : 'flex-start'}>
{!!isOutput && (
<Stack.Item>
<DisplayName
port={port}
isOutput={isOutput}
componentId={componentId}
portIndex={portIndex} />
</Stack.Item>
)}
<Stack.Item>
<Icon
color={port.color || 'blue'}
name={'circle'}
position="relative"
onMouseDown={this.handlePortMouseDown}
onContextMenu={this.handlePortRightClick}
onMouseUp={this.handlePortMouseUp}
>
<span ref={this.iconRef} className="ObjectComponent__PortPos" />
</Icon>
</Stack.Item>
{!isOutput && (
<Stack.Item>
<DisplayName
port={port}
isOutput={isOutput}
componentId={componentId}
portIndex={portIndex} />
</Stack.Item>
)}
</Stack>
);
}
}
@@ -0,0 +1,5 @@
export const NULL_REF = '[0x0]';
export const ABSOLUTE_Y_OFFSET = -32;
export const SVG_CURVE_INTENSITY = 64;
export const MOUSE_BUTTON_LEFT = 0;
@@ -1,142 +1,41 @@
import { useBackend, useLocalState } from '../../backend';
import { useBackend } from '../../backend';
import {
Box,
Stack,
Icon,
Button,
Input,
Flex,
NumberInput,
Dropdown,
InfinitePlane,
Stack,
Box,
Button,
} from '../../components';
import { Component, createRef } from 'inferno';
import { Component } from 'inferno';
import { Window } from '../../layouts';
import { CSS_COLORS } from '../../constants';
import { classes, shallowDiffers } from '../../../common/react';
import { resolveAsset } from '../../assets';
import { CircuitInfo } from './CircuitInfo';
const NULL_REF = '[0x0]';
const ABSOLUTE_Y_OFFSET = -32;
const SVG_CURVE_INTENSITY = 64;
const BasicInput = (props, context) => {
const { children, name, setValue, defaultValue, value } = props;
return (
(value !== null && (
<Stack onMouseDown={(e) => e.stopPropagation()}>
<Stack.Item>
<Button
color="transparent"
compact
icon="times"
onClick={() => setValue(null, { set_null: true })}
/>
</Stack.Item>
<Stack.Item>{children}</Stack.Item>
</Stack>
)) || (
<Button
content={name}
color="transparent"
compact
onClick={() => setValue(defaultValue)}
/>
)
);
};
const FUNDAMENTAL_DATA_TYPES = {
'string': (props, context) => {
const { name, value, setValue, color } = props;
return (
<BasicInput name={name} setValue={setValue} value={value} defaultValue="">
<Input
placeholder={name}
value={value}
onChange={(e, val) => setValue(val)}
/>
</BasicInput>
);
},
'number': (props, context) => {
const { name, value, setValue, color } = props;
return (
<BasicInput
name={name}
setValue={setValue}
value={value}
defaultValue={0}>
<NumberInput
value={value}
color={color}
onChange={(e, val) => setValue(val)}
unit={name}
/>
</BasicInput>
);
},
'entity': (props, context) => {
const { name, setValue, color } = props;
return (
<Button
content={name}
color="transparent"
icon="upload"
compact
onClick={() => setValue(null, { marked_atom: true })}
/>
);
},
'signal': (props, context) => {
const { name, setValue } = props;
return (
<Button
content={name}
color="transparent"
compact
onClick={() => setValue()}
/>
);
},
'any': (props, context) => {
const { name, value, setValue, color } = props;
return (
<BasicInput
name={name}
setValue={setValue}
value={value}
defaultValue={''}>
<Stack>
<Stack.Item>
<Button
color={color}
icon="upload"
onClick={() => setValue(null, { marked_atom: true })}
/>
</Stack.Item>
<Stack.Item>
<Input
placeholder={name}
value={value}
onChange={(e, val) => setValue(val)}
/>
</Stack.Item>
</Stack>
</BasicInput>
);
},
};
import { NULL_REF, ABSOLUTE_Y_OFFSET, MOUSE_BUTTON_LEFT } from './constants';
import { Connections } from './Connections';
import { ObjectComponent } from './ObjectComponent';
export class IntegratedCircuit extends Component {
constructor() {
super();
this.state = {
locations: {},
selectedPort: null,
mouseX: null,
mouseY: null,
zoom: 1,
backgroundX: 0,
backgroundY: 0,
};
this.handlePortLocation = this.handlePortLocation.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handlePortClick = this.handlePortClick.bind(this);
this.handlePortRightClick = this.handlePortRightClick.bind(this);
this.handlePortUp = this.handlePortUp.bind(this);
this.handlePortDrag = this.handlePortDrag.bind(this);
this.handlePortRelease = this.handlePortRelease.bind(this);
this.handleZoomChange = this.handleZoomChange.bind(this);
this.handleBackgroundMoved = this.handleBackgroundMoved.bind(this);
}
// Helper function to get an element's exact position
@@ -179,6 +78,99 @@ export class IntegratedCircuit extends Component {
this.setState({ locations: locations });
}
handlePortClick(portIndex, componentId, port, isOutput, event) {
if (event.button !== MOUSE_BUTTON_LEFT) {
return;
}
event.stopPropagation();
this.setState({
selectedPort: {
index: portIndex,
component_id: componentId,
is_output: isOutput,
ref: port.ref,
},
});
this.handlePortDrag(event);
window.addEventListener('mousemove', this.handlePortDrag);
window.addEventListener('mouseup', this.handlePortRelease);
}
// mouse up called whilst over a port. This means we can check if selectedPort
// exists and do perform some actions if it does.
handlePortUp(portIndex, componentId, port, isOutput, event) {
const { act } = useBackend(this.context);
const {
selectedPort,
} = this.state;
if (!selectedPort) {
return;
}
if (selectedPort.is_output === isOutput) {
return;
}
let data;
if (isOutput) {
data = {
input_port_id: selectedPort.index,
output_port_id: portIndex,
input_component_id: selectedPort.component_id,
output_component_id: componentId,
};
} else {
data = {
input_port_id: portIndex,
output_port_id: selectedPort.index,
input_component_id: componentId,
output_component_id: selectedPort.component_id,
};
}
act("add_connection", data);
}
handlePortDrag(event) {
this.setState((state) => ({
mouseX: event.clientX - state.backgroundX,
mouseY: event.clientY - state.backgroundY,
}));
}
handlePortRelease(event) {
this.setState({
selectedPort: null,
});
window.removeEventListener('mousemove', this.handlePortDrag);
window.removeEventListener('mouseup', this.handlePortRelease);
}
handlePortRightClick(portIndex, componentId, port, isOutput, event) {
const { act } = useBackend(this.context);
event.preventDefault();
act('remove_connection', {
component_id: componentId,
is_input: !isOutput,
port_id: portIndex,
});
}
handleZoomChange(newZoom) {
this.setState({
zoom: newZoom,
});
}
handleBackgroundMoved(newX, newY) {
this.setState({
backgroundX: newX,
backgroundY: newY,
});
}
componentDidMount() {
window.addEventListener('mousedown', this.handleMouseDown);
}
@@ -207,7 +199,40 @@ export class IntegratedCircuit extends Component {
examined_rel_y,
is_admin,
} = data;
const { locations } = this.state;
const { locations, selectedPort } = this.state;
const connections = [];
for (const comp of components) {
if (comp === null) {
continue;
}
for (const port of comp.input_ports) {
if (port.connected_to === NULL_REF
|| selectedPort?.ref === port.ref) continue;
const output_port = locations[port.connected_to];
connections.push({
color: (output_port && output_port.color) || 'blue',
from: output_port,
to: locations[port.ref],
});
}
}
if (selectedPort) {
const { mouseX, mouseY, zoom } = this.state;
const isOutput = selectedPort.is_output;
const portLocation = locations[selectedPort.ref];
const mouseCoords = {
x: (mouseX)*Math.pow(zoom, -1),
y: (mouseY + ABSOLUTE_Y_OFFSET)*Math.pow(zoom, -1),
};
connections.push({
color: (portLocation && portLocation.color) || 'blue',
from: isOutput? portLocation : mouseCoords,
to: isOutput? mouseCoords : portLocation,
});
}
return (
<Window
@@ -247,7 +272,10 @@ export class IntegratedCircuit extends Component {
width="100%"
height="100%"
backgroundImage={resolveAsset('grid_background.png')}
imageWidth={900}>
imageWidth={900}
onZoomChange={this.handleZoomChange}
onBackgroundMoved={this.handleBackgroundMoved}
>
{components.map(
(comp, index) =>
comp && (
@@ -257,10 +285,13 @@ export class IntegratedCircuit extends Component {
index={index + 1}
onPortUpdated={this.handlePortLocation}
onPortLoaded={this.handlePortLocation}
onPortMouseDown={this.handlePortClick}
onPortRightClick={this.handlePortRightClick}
onPortMouseUp={this.handlePortUp}
/>
)
)}
<Connections locations={locations} />
<Connections connections={connections} />
</InfinitePlane>
{!!examined_name && (
<CircuitInfo
@@ -279,465 +310,3 @@ export class IntegratedCircuit extends Component {
}
}
const Connections = (props, context) => {
const { data } = useBackend(context);
const { locations } = props;
const { components } = data;
const connections = [];
for (const comp of components) {
if (comp === null) {
continue;
}
for (const port of comp.input_ports) {
if (port.connected_to === NULL_REF) continue;
const output_port = locations[port.connected_to];
connections.push({
color: (output_port && output_port.color) || 'blue',
from: output_port,
to: locations[port.ref],
});
}
}
const isColorClass = (str) => {
if (typeof str === 'string') {
return CSS_COLORS.includes(str);
}
};
return (
<svg
width="100%"
height="100%"
style={{
'position': 'absolute',
'pointer-events': 'none',
'z-index': -1,
}}>
{connections.map((val, index) => {
const from = val.from;
const to = val.to;
if (!to || !from) {
return;
}
// Starting point
let path = `M ${from.x} ${from.y}`;
path += `C ${from.x + SVG_CURVE_INTENSITY}, ${from.y},`;
path += `${to.x - SVG_CURVE_INTENSITY}, ${to.y},`;
path += `${to.x}, ${to.y}`;
val.color = val.color || 'blue';
return (
<path
className={classes([
isColorClass(val.color) && `color-stroke-${val.color}`,
])}
key={index}
d={path}
fill="transparent"
stroke-width="2px"
/>
);
})}
</svg>
);
};
export class ObjectComponent extends Component {
constructor() {
super();
this.state = {
isDragging: false,
dragPos: null,
startPos: null,
lastMousePos: null,
};
this.handleStartDrag = this.handleStartDrag.bind(this);
this.handleStopDrag = this.handleStopDrag.bind(this);
this.handleDrag = this.handleDrag.bind(this);
}
handleStartDrag(e) {
const { x, y } = this.props;
e.stopPropagation();
this.setState({
lastMousePos: null,
isDragging: true,
dragPos: { x: x, y: y },
startPos: { x: x, y: y },
});
window.addEventListener('mousemove', this.handleDrag);
window.addEventListener('mouseup', this.handleStopDrag);
}
handleStopDrag(e) {
const { act } = useBackend(this.context);
const { dragPos } = this.state;
const { index } = this.props;
if (dragPos) {
act('set_component_coordinates', {
component_id: index,
rel_x: dragPos.x,
rel_y: dragPos.y,
});
}
window.removeEventListener('mousemove', this.handleDrag);
window.removeEventListener('mouseup', this.handleStopDrag);
this.setState({ isDragging: false });
}
handleDrag(e) {
const { dragPos, isDragging, lastMousePos } = this.state;
if (dragPos && isDragging) {
e.preventDefault();
const { screenZoomX, screenZoomY, screenX, screenY } = e;
let xPos = screenZoomX || screenX;
let yPos = screenZoomY || screenY;
if (lastMousePos) {
this.setState({
dragPos: {
x: dragPos.x - (lastMousePos.x - xPos),
y: dragPos.y - (lastMousePos.y - yPos),
},
});
}
this.setState({
lastMousePos: { x: xPos, y: yPos },
});
}
}
shouldComponentUpdate(nextProps, nextState) {
const { input_ports, output_ports } = this.props;
return (
shallowDiffers(this.props, nextProps)
|| shallowDiffers(this.state, nextState)
|| shallowDiffers(input_ports, nextProps.input_ports)
|| shallowDiffers(output_ports, nextProps.output_ports)
);
}
render() {
const {
input_ports,
output_ports,
name,
x,
y,
index,
color = 'blue',
options,
option,
removable,
locations,
onPortUpdated,
onPortLoaded,
...rest
} = this.props;
const { act } = useBackend(this.context);
const { startPos, dragPos } = this.state;
let [x_pos, y_pos] = [x, y];
if (dragPos && startPos && startPos.x === x_pos && startPos.y === y_pos) {
x_pos = dragPos.x;
y_pos = dragPos.y;
}
return (
<Box
{...rest}
position="absolute"
left={`${x_pos}px`}
top={`${y_pos}px`}
onMouseDown={this.handleStartDrag}
onMouseUp={this.handleStopDrag}
onComponentWillUnmount={this.handleDrag}>
<Box
backgroundColor={color}
py={1}
px={1}
className="ObjectComponent__Titlebar">
<Stack>
<Stack.Item grow={1} unselectable="on">
{name}
</Stack.Item>
{!!options && (
<Stack.Item>
<Dropdown
color={color}
nochevron
over
options={options}
displayText={option}
noscroll
onSelected={(selected) =>
act('set_component_option', {
component_id: index,
option: selected,
})}
/>
</Stack.Item>
)}
<Stack.Item>
<Button
color="transparent"
icon="info"
compact
onClick={(e) =>
act('set_examined_component', {
component_id: index,
x: e.pageX,
y: e.pageY + ABSOLUTE_Y_OFFSET,
})}
/>
</Stack.Item>
{!!removable && (
<Stack.Item>
<Button
color="transparent"
icon="times"
compact
onClick={() =>
act('detach_component', { component_id: index })}
/>
</Stack.Item>
)}
</Stack>
</Box>
<Box
className="ObjectComponent__Content"
unselectable="on"
py={1}
px={1}>
<Stack>
<Stack.Item grow={1}>
<Stack vertical fill>
{input_ports.map((port, portIndex) => (
<Stack.Item key={portIndex}>
<Port
port={port}
portIndex={portIndex + 1}
componentId={index}
onPortLoaded={onPortLoaded}
onPortUpdated={onPortUpdated}
/>
</Stack.Item>
))}
</Stack>
</Stack.Item>
<Stack.Item ml={5}>
<Stack vertical>
{output_ports.map((port, portIndex) => (
<Stack.Item key={portIndex}>
<Port
port={port}
portIndex={portIndex + 1}
componentId={index}
onPortLoaded={onPortLoaded}
onPortUpdated={onPortUpdated}
isOutput
/>
</Stack.Item>
))}
</Stack>
</Stack.Item>
</Stack>
</Box>
</Box>
);
}
}
export class Port extends Component {
constructor() {
super();
this.iconRef = createRef();
this.componentDidUpdate = this.componentDidUpdate.bind(this);
this.componentDidMount = this.componentDidMount.bind(this);
this.handlePortClick = this.handlePortClick.bind(this);
this.handlePortRightClick = this.handlePortRightClick.bind(this);
}
handlePortClick() {
const { act } = useBackend(this.context);
const [selectedPort, setSelectedPort] = useLocalState(
this.context,
'selected_port',
null
);
const { port, portIndex, componentId, isOutput } = this.props;
if (selectedPort) {
if (selectedPort.ref === port.ref) {
setSelectedPort(null);
return;
} else {
if (selectedPort.is_output === isOutput) {
setSelectedPort(null);
return;
}
let data;
if (isOutput) {
data = {
input_port_id: selectedPort.index,
output_port_id: portIndex,
input_component_id: selectedPort.component_id,
output_component_id: componentId,
};
} else {
data = {
input_port_id: portIndex,
output_port_id: selectedPort.index,
input_component_id: componentId,
output_component_id: selectedPort.component_id,
};
}
act('add_connection', data);
setSelectedPort(null);
return;
}
}
setSelectedPort({
index: portIndex,
component_id: componentId,
is_output: isOutput,
ref: port.ref,
});
}
handlePortRightClick(e) {
const { act } = useBackend(this.context);
const { port, portIndex, componentId, isOutput, ...rest } = this.props;
e.preventDefault();
act('remove_connection', {
component_id: componentId,
is_input: !isOutput,
port_id: portIndex,
});
}
componentDidUpdate() {
const { port, onPortUpdated } = this.props;
if (onPortUpdated) {
onPortUpdated(port, this.iconRef.current);
}
}
componentDidMount() {
const { port, onPortLoaded } = this.props;
if (onPortLoaded) {
onPortLoaded(port, this.iconRef.current);
}
}
render() {
const { port, portIndex, componentId, isOutput, ...rest } = this.props;
const [selectedPort, setSelectedPort] = useLocalState(
this.context,
'selected_port',
null
);
return (
<Stack {...rest} justify={isOutput ? 'flex-end' : 'flex-start'}>
{!!isOutput && (
<Stack.Item>
<DisplayName
port={port}
isOutput={isOutput}
componentId={componentId}
portIndex={portIndex}
/>
</Stack.Item>
)}
<Stack.Item>
<Icon
color={port.color || 'blue'}
name={
selectedPort && selectedPort.ref === port.ref
? 'dot-circle'
: 'circle'
}
position="relative"
onClick={this.handlePortClick}
onContextMenu={this.handlePortRightClick}>
<span ref={this.iconRef} className="ObjectComponent__PortPos" />
</Icon>
</Stack.Item>
{!isOutput && (
<Stack.Item>
<DisplayName
port={port}
isOutput={isOutput}
componentId={componentId}
portIndex={portIndex}
/>
</Stack.Item>
)}
</Stack>
);
}
}
const DisplayName = (props, context) => {
const { act } = useBackend(context);
const { port, isOutput, componentId, portIndex, ...rest } = props;
const InputComponent = FUNDAMENTAL_DATA_TYPES[port.type || 'any'];
const hasInput
= !isOutput && port.connected_to === NULL_REF && InputComponent;
return (
<Box {...rest}>
<Flex direction="column">
<Flex.Item>
{(hasInput && (
<InputComponent
setValue={(val, extraParams) =>
act('set_component_input', {
component_id: componentId,
port_id: portIndex,
input: val,
...extraParams,
})}
color={port.color}
name={port.name}
value={port.current_data}
/>
))
|| (isOutput && (
<Button
compact
color="transparent"
onClick={() =>
act('get_component_value', {
component_id: componentId,
port_id: portIndex,
})}>
<Box color="white">{port.name}</Box>
</Button>
))
|| port.name}
</Flex.Item>
<Flex.Item>
<Box
fontSize={0.75}
opacity={0.25}
textAlign={isOutput ? 'right' : 'left'}>
{port.type || 'any'}
</Box>
</Flex.Item>
</Flex>
</Box>
);
};