@bitbloq/3d
Version:
One Platform to Rule Them All
508 lines (451 loc) • 13.5 kB
JavaScript
import React from 'react';
import { connect } from 'react-redux';
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import { Spring } from 'react-spring';
import { DragDropContext } from 'react-beautiful-dnd';
import {
updateObject,
updateObjectParameter,
updateObjectViewOption,
updateOperation,
createObject,
deleteObject,
undoComposition,
duplicateObject,
addOperation,
removeOperation,
reorderOperation,
setActiveOperation,
unsetActiveOperation,
ungroup,
convertToGroup,
} from '../../actions/threed';
import { getObjects, getSelectedObjects } from '../../reducers/threed/';
import { colors } from '../../base-styles';
import PropertyInput from './PropertyInput';
import OperationsList from './OperationsList';
import { DropDown, Icon, Input, Tooltip, withTranslate } from '@bitbloq/ui';
import config from '../../config/threed';
const Wrap = styled.div`
display: flex;
`;
const Container = styled.div`
width: 310px;
min-width: 310px;
overflow: hidden;
border-left: 1px solid #cfcfcf;
background-color: white;
display: flex;
flex-direction: column;
`;
const Header = styled.div`
height: 50px;
padding: 0px 20px;
background-color: white;
border-bottom: 1px solid #cfcfcf;
display: flex;
align-items: center;
font-size: 1.1em;
img {
cursor: pointer;
}
`;
const HeaderIcon = styled.div`
margin-right: 10px;
color: #4dc3ff;
svg {
width: 30px;
height: auto;
}
`;
const ObjectName = styled.div`
flex: 1;
display: flex;
align-items: center;
font-weigth: bold;
font-style: italic;
font-size: 13px;
cursor: pointer;
svg {
display: none;
margin-left: 10px;
width: 14px;
height: auto;
}
&:hover {
svg {
display: block;
}
}
`;
const ObjectProperties = styled.div`
border-bottom: 1px solid #cfcfcf;
position: relative;
font-size: 13px;
`;
const ParametersPanel = styled.div`
padding: 20px;
`;
const ParametersForm = styled.div`
flex: 1;
`;
const ObjectButtons = styled.div`
display: flex;
padding: 0px 4px 10px 4px;
`;
const OperationButton = styled.div`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 50px;
border-radius: 3px;
background-color: #ebebeb;
flex: 1;
margin: 0px 2px;
border-bottom: 5px solid ${props => props.color};
svg {
width: 24px;
height: auto;
}
`;
const NameInput = styled(Input)`
height: 30px;
padding: 6px 8px;
`;
const ContextButton = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
border-radius: 4px;
background-color: white;
cursor: pointer;
${props =>
props.isOpen &&
css`
border: solid 1px #dddddd;
background-color: #e8e8e8;
`} svg {
transform: rotate(90deg);
}
`;
const ContextMenu = styled.div`
background-color: white;
margin-top: 6px;
box-shadow: 0 3px 7px 0 rgba(0, 0, 0, 0.5);
border-radius: 4px;
border: solid 1px #cfcfcf;
`;
const ContextMenuOption = styled.div`
width: 220px;
display: flex;
align-items: center;
height: 34px;
border-bottom: 1px solid #ebebeb;
font-size: 14px;
cursor: pointer;
svg {
margin: 0px 14px;
width: 13px;
height: auto;
}
&:hover {
background-color: #ebebeb;
}
&:last-child {
border: none;
}
${props =>
props.danger &&
css`
color: #d82b32;
`};
`;
class PropertiesPanel extends React.Component {
state = {
draggingOperations: false,
contextMenuOpen: false,
editingName: false,
};
nameInputRef = React.createRef();
componentDidUpdate(prevProps, prevState) {
if (this.state.editingName && !prevState.editingName) {
this.nameInputRef.current.focus();
}
}
onObjectNameChange = (object, name) => {
this.props.updateObjectViewOption(object, 'name', name);
};
onObjectParameterChange = (object, parameter, value) => {
if (parameter.name === 'baseObject') {
const newChildren = object.children.filter(c => c !== value);
newChildren.unshift(value);
this.props.updateObject({
...object,
children: newChildren,
});
} else if (parameter.isViewOption) {
this.props.updateObjectViewOption(object, parameter.name, value);
} else {
this.props.updateObjectParameter(object, parameter.name, value);
}
};
onOperationParameterChange = (object, operation, parameter, value) => {
if (parameter.setValue) {
this.props.updateOperation(object, parameter.setValue(operation, value));
} else {
this.props.updateOperation(object, {
...operation,
[parameter.name]: value,
});
}
};
onDragStart = () => {
this.setState({ draggingOperations: true });
};
onDragEnd = (result, object) => {
const { destination, source, draggableId } = result;
const operation = object.operations.find(({ id }) => id === draggableId);
this.setState({ draggingOperations: false });
if (!destination || !operation) return;
this.props.reorderOperation(
object,
operation,
source.index,
destination.index,
);
};
onRenameClick = () => {
this.setState({ editingName: true });
};
onDeleteClick = () => {
const { object, deleteObject } = this.props;
deleteObject(object);
};
onDuplicateClick = () => {
const { object, duplicateObject } = this.props;
duplicateObject(object);
};
onUngroupClick = () => {
const { object, ungroup } = this.props;
ungroup(object);
};
onConvertToGroupClick = () => {
const { object, convertToGroup } = this.props;
convertToGroup(object);
};
onUndoClick = () => {
const { object, undoComposition } = this.props;
undoComposition(object);
};
onAddOperation(object, operation) {
this.props.addOperation(object, operation.create());
}
onRemoveOperation(object, operation) {
this.props.removeOperation(object, operation);
}
renderObjectPanel(object) {
const { draggingOperations, contextMenuOpen, editingName } = this.state;
const { t } = this.props;
const {
setActiveOperation,
unsetActiveOperation,
advancedMode,
isTopObject,
} = this.props;
const { color } = object.viewOptions;
const typeConfig =
config.objectTypes.find(s => s.name === object.type) || {};
const { parameters: baseParameters, icon } = typeConfig;
const { canUndo, undoLabel, canUngroup, canConverToGroup } = typeConfig;
const parameters = [...baseParameters(object)];
if (typeConfig.showBaseObject) {
parameters.push({
name: 'baseObject',
label: 'param-base-object',
type: 'select',
options: object.children.map(child => ({
label: child.viewOptions.name,
value: child,
})),
});
}
if (!typeConfig.withoutColor) {
parameters.push({
name: 'color',
label: 'param-color',
type: 'color',
isViewOption: true,
});
}
return (
<DragDropContext
onDragStart={this.onDragStart}
onDragEnd={result => this.onDragEnd(result, object)}
>
<Header>
<HeaderIcon>{icon}</HeaderIcon>
{editingName && (
<NameInput
type="text"
ref={this.nameInputRef}
value={object.viewOptions.name}
onChange={e => this.onObjectNameChange(object, e.target.value)}
onBlur={() => this.setState({ editingName: false })}
/>
)}
{!editingName && (
<ObjectName onClick={() => this.setState({ editingName: true })}>
{object.viewOptions.name} <Icon name="pencil" />
</ObjectName>
)}
<DropDown>
{isOpen => (
<ContextButton isOpen={isOpen}>
<Icon name="ellipsis" />
</ContextButton>
)}
<ContextMenu>
{isTopObject && (
<ContextMenuOption onClick={this.onDuplicateClick}>
<Icon name="duplicate" /> {t('menu-duplicate')}
</ContextMenuOption>
)}
<ContextMenuOption onClick={this.onRenameClick}>
<Icon name="pencil" /> {t('menu-rename')}
</ContextMenuOption>
{canUngroup && (
<ContextMenuOption onClick={this.onUngroupClick}>
<Icon name="ungroup" /> {t('menu-ungroup')}
</ContextMenuOption>
)}
{canConverToGroup && (
<ContextMenuOption onClick={this.onConvertToGroupClick}>
<Icon name="group" /> {t('menu-convert-to-group')}
</ContextMenuOption>
)}
{canUndo && (
<ContextMenuOption onClick={this.onUndoClick}>
<Icon name="undo" /> {t(undoLabel)}
</ContextMenuOption>
)}
{isTopObject && (
<ContextMenuOption danger={true} onClick={this.onDeleteClick}>
<Icon name="trash" /> {t('menu-delete')}
</ContextMenuOption>
)}
</ContextMenu>
</DropDown>
</Header>
<ObjectProperties>
<ParametersPanel>
{parameters.map(parameter => (
<PropertyInput
key={parameter.name}
parameter={parameter}
value={
parameter.isViewOption
? object.viewOptions && object.viewOptions[parameter.name]
: object.parameters && object.parameters[parameter.name]
}
onChange={value =>
this.onObjectParameterChange(object, parameter, value)
}
/>
))}
</ParametersPanel>
{advancedMode && (
<ObjectButtons>
{config.objectOperations.map(operation => (
<Tooltip key={operation.name} content={t(operation.label)}>
{tooltipProps => (
<OperationButton
{...tooltipProps}
color={operation.color}
onClick={() => this.onAddOperation(object, operation)}
>
{operation.icon}
</OperationButton>
)}
</Tooltip>
))}
</ObjectButtons>
)}
</ObjectProperties>
<OperationsList
operations={object.operations}
advancedMode={advancedMode}
onParameterChange={(operation, parameter, value) =>
this.onOperationParameterChange(object, operation, parameter, value)
}
onParameterFocus={(operation, parameter) => {
if (parameter.activeOperation) {
setActiveOperation(parameter.activeOperation(object, operation));
}
}}
onParameterBlur={(operation, parameter) => {
if (parameter.activeOperation) {
unsetActiveOperation();
}
}}
onRemoveOperation={operation =>
this.onRemoveOperation(object, operation)
}
/>
</DragDropContext>
);
}
render() {
const { object } = this.props;
return (
<Spring from={{ width: 0 }} to={{ width: object ? 310 : 0 }}>
{style => (
<Wrap style={style}>
<Container>{object && this.renderObjectPanel(object)}</Container>
</Wrap>
)}
</Spring>
);
}
}
const mapStateToProps = ({ threed }) => {
const topObjects = threed.scene.objects;
const selectedObjects = getSelectedObjects(threed) || [];
const object = selectedObjects.length === 1 ? selectedObjects[0] : null;
return {
object,
advancedMode: threed.ui.advancedMode,
isTopObject: topObjects.includes(object),
};
};
const mapDispatchToProps = dispatch => ({
updateObject: object => dispatch(updateObject(object)),
updateObjectParameter: (object, parameter, value) =>
dispatch(updateObjectParameter(object, parameter, value)),
updateObjectViewOption: (object, option, value) =>
dispatch(updateObjectViewOption(object, option, value)),
updateOperation: (object, operation, parameter, value) =>
dispatch(updateOperation(object, operation, parameter, value)),
createObject: object => dispatch(createObject(object)),
deleteObject: object => dispatch(deleteObject(object)),
undoComposition: object => dispatch(undoComposition(object)),
ungroup: object => dispatch(ungroup(object)),
convertToGroup: object => dispatch(convertToGroup(object)),
duplicateObject: object => dispatch(duplicateObject(object)),
addOperation: (object, operation) =>
dispatch(addOperation(object, operation)),
removeOperation: (object, operation) =>
dispatch(removeOperation(object, operation)),
reorderOperation: (object, operation, from, to) =>
dispatch(reorderOperation(object, operation, from, to)),
setActiveOperation: ({ object, type, axis, relative }) =>
dispatch(setActiveOperation(object, type, axis, relative)),
unsetActiveOperation: () => dispatch(unsetActiveOperation()),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withTranslate(PropertiesPanel));