UNPKG

react-floorplanner

Version:

react-floorplanner is a React Component for plans design. Draw a 2D floorplan and navigate it in 3D mode.

420 lines (340 loc) 14.4 kB
import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {Map, fromJS} from 'immutable'; import FormSubmitButton from '../../style/form-submit-button'; import CancelButton from '../../style/cancel-button'; import DeleteButton from '../../style/delete-button'; import AttributesEditor from './attributes-editor/attributes-editor'; import * as geometry from '../../../utils/geometry.js'; import * as math from '../../../utils/math.js'; import * as SharedStyle from '../../../shared-style'; import convert from 'convert-units'; import MdContentCopy from 'react-icons/lib/md/content-copy'; import MdContentPaste from 'react-icons/lib/md/content-paste'; import diff from 'immutablediff'; const PRECISION = 2; const attrPorpSeparatorStyle = { margin: '0.5em 0.25em 0.5em 0', border: '2px solid ' + SharedStyle.SECONDARY_COLOR.alt, position:'relative', height:'2.5em', borderRadius:'2px' }; const headActionStyle = { position:'absolute', right:'0.5em', top:'0.5em' }; const iconHeadStyle = { float:'right', margin:'-3px 4px 0px 0px', padding:0, cursor:'pointer', fontSize:'1.4em' }; export default class ElementEditor extends Component { constructor(props, context) { super(props, context); this.state = { attributesFormData: this.initAttrData(this.props.element, this.props.layer, this.props.state), propertiesFormData: this.initPropData(this.props.element, this.props.layer, this.props.state) }; this.updateAttribute = this.updateAttribute.bind(this); } shouldComponentUpdate(nextProps, nextState) { let { attributesFormData : oldAttribute, propertiesFormData : oldProperties } = this.state; let { attributesFormData : newAttribute, propertiesFormData : newProperties } = nextState; if( diff( oldAttribute, newAttribute ).size ) return true; if( diff( oldProperties, newProperties ).size ) return true; if( diff( this.props.state.clipboardProperties, nextProps.state.clipboardProperties ).size ) return true; return false; } componentWillReceiveProps({ element, layer, state }) { let { prototype, id } = element; let scene = this.props.state.get('scene'); let selectedLayer = scene.getIn(['layers', scene.get('selectedLayer')]); let selected = selectedLayer.getIn([prototype, id]); if( diff( selectedLayer, layer ).size ) this.setState({ attributesFormData: this.initAttrData(element, layer, state), propertiesFormData: this.initPropData(element, layer, state) }); } initAttrData(element, layer, state) { element = typeof element.misc === 'object' ? element.set('misc', new Map(element.misc)) : element; switch (element.prototype) { case 'items': { return new Map(element); } case 'lines': { let v_a = layer.vertices.get(element.vertices.get(0)); let v_b = layer.vertices.get(element.vertices.get(1)); let distance = geometry.pointsDistance(v_a.x, v_a.y, v_b.x, v_b.y); let _unit = element.misc.get('_unitLength') || this.context.catalog.unit; let _length = convert(distance).from(this.context.catalog.unit).to(_unit); return new Map({ vertexOne: v_a, vertexTwo: v_b, lineLength: new Map({length: distance, _length, _unit}), }); } case 'holes': { let line = layer.lines.get(element.line); let {x: x0, y: y0} = layer.vertices.get(line.vertices.get(0)); let {x: x1, y: y1} = layer.vertices.get(line.vertices.get(1)); let lineLength = geometry.pointsDistance(x0, y0, x1, y1); let startAt = lineLength * element.offset - element.properties.get('width').get('length') / 2; let _unitA = element.misc.get('_unitA') || this.context.catalog.unit; let _lengthA = convert(startAt).from(this.context.catalog.unit).to(_unitA); let endAt = lineLength - lineLength * element.offset - element.properties.get('width').get('length') / 2; let _unitB = element.misc.get('_unitB') || this.context.catalog.unit; let _lengthB = convert(endAt).from(this.context.catalog.unit).to(_unitB); return new Map({ offset: element.offset, offsetA: new Map({ length: math.toFixedFloat(startAt, PRECISION), _length: math.toFixedFloat(_lengthA, PRECISION), _unit: _unitA }), offsetB: new Map({ length: math.toFixedFloat(endAt, PRECISION), _length: math.toFixedFloat(_lengthB, PRECISION), _unit: _unitB }) }); } case 'areas': { return new Map({}); } default: return null; } } initPropData(element, layer, state) { let {catalog} = this.context; let catalogElement = catalog.getElement(element.type); let mapped = {}; for (let name in catalogElement.properties) { mapped[name] = new Map({ currentValue: element.properties.has(name) ? element.properties.get(name) : fromJS(catalogElement.properties[name].defaultValue), configs: catalogElement.properties[name] }); } return new Map(mapped); } updateAttribute(attributeName, value) { let {attributesFormData} = this.state; switch (this.props.element.prototype) { case 'items': { attributesFormData = attributesFormData.set(attributeName, value); break; } case 'lines': { switch(attributeName) { case 'lineLength': { let v_0 = attributesFormData.get('vertexOne'); let v_1 = attributesFormData.get('vertexTwo'); let [v_a, v_b] = geometry.orderVertices([v_0, v_1]); let v_b_new = geometry.extendLine(v_a.x, v_a.y, v_b.x, v_b.y, value.get('length'), PRECISION); attributesFormData = attributesFormData.withMutations(attr => { attr.set(v_0 === v_a ? 'vertexTwo' : 'vertexOne', v_b.merge(v_b_new)); attr.set('lineLength', value); }); break; } case 'vertexOne': case 'vertexTwo': { attributesFormData = attributesFormData.withMutations(attr => { attr.set(attributeName, attr.get(attributeName).merge(value)); let newDistance = geometry.verticesDistance(attr.get('vertexOne'), attr.get('vertexTwo')); attr.mergeIn(['lineLength'], attr.get('lineLength').merge({ 'length': newDistance, '_length': convert(newDistance).from(this.context.catalog.unit).to(attr.get('lineLength').get('_unit')) })); }); break; } default: { attributesFormData = attributesFormData.set(attributeName, value); break; } } break; } case 'holes': { switch( attributeName ) { case 'offsetA': { let line = this.props.layer.lines.get(this.props.element.line); let orderedVertices = geometry.orderVertices([ this.props.layer.vertices.get(line.vertices.get(0)), this.props.layer.vertices.get(line.vertices.get(1)) ]); let [ {x: x0, y: y0}, {x: x1, y: y1} ] = orderedVertices; let alpha = geometry.angleBetweenTwoPoints(x0, y0, x1, y1); let lineLength = geometry.pointsDistance(x0, y0, x1, y1); let widthLength = this.props.element.properties.get('width').get('length'); let halfWidthLength = widthLength / 2; let lengthValue = value.get('length'); lengthValue = Math.max(lengthValue, 0); lengthValue = Math.min(lengthValue, lineLength - widthLength); let xp = (lengthValue + halfWidthLength) * Math.cos(alpha) + x0; let yp = (lengthValue + halfWidthLength) * Math.sin(alpha) + y0; let offset = geometry.pointPositionOnLineSegment(x0, y0, x1, y1, xp, yp); let endAt = math.toFixedFloat(lineLength - (lineLength * offset) - halfWidthLength, PRECISION); let offsetUnit = attributesFormData.getIn(['offsetB', '_unit']); let offsetB = new Map({ length: endAt, _length: convert(endAt).from(this.context.catalog.unit).to(offsetUnit), _unit: offsetUnit }); attributesFormData = attributesFormData.set('offsetB', offsetB).set('offset', offset); let offsetAttribute = new Map({ length: math.toFixedFloat(lengthValue, PRECISION), _unit: value.get('_unit'), _length: math.toFixedFloat(convert(lengthValue).from(this.context.catalog.unit).to(value.get('_unit')), PRECISION) }); attributesFormData = attributesFormData.set(attributeName, offsetAttribute); break; } case 'offsetB': { let line = this.props.layer.lines.get(this.props.element.line); let orderedVertices = geometry.orderVertices([ this.props.layer.vertices.get(line.vertices.get(0)), this.props.layer.vertices.get(line.vertices.get(1)) ]); let [ {x: x0, y: y0}, {x: x1, y: y1} ] = orderedVertices; let alpha = geometry.angleBetweenTwoPoints(x0, y0, x1, y1); let lineLength = geometry.pointsDistance(x0, y0, x1, y1); let widthLength = this.props.element.properties.get('width').get('length'); let halfWidthLength = widthLength / 2; let lengthValue = value.get('length'); lengthValue = Math.max(lengthValue, 0); lengthValue = Math.min(lengthValue, lineLength - widthLength); let xp = x1 - (lengthValue + halfWidthLength) * Math.cos(alpha); let yp = y1 - (lengthValue + halfWidthLength) * Math.sin(alpha); let offset = geometry.pointPositionOnLineSegment(x0, y0, x1, y1, xp, yp); let startAt = math.toFixedFloat((lineLength * offset) - halfWidthLength, PRECISION); let offsetUnit = attributesFormData.getIn(['offsetA', '_unit']); let offsetA = new Map({ length: startAt, _length: convert(startAt).from(this.context.catalog.unit).to(offsetUnit), _unit: offsetUnit }); attributesFormData = attributesFormData.set('offsetA', offsetA).set('offset', offset); let offsetAttribute = new Map({ length: math.toFixedFloat(lengthValue, PRECISION), _unit: value.get('_unit'), _length: math.toFixedFloat(convert(lengthValue).from(this.context.catalog.unit).to(value.get('_unit')), PRECISION) }); attributesFormData = attributesFormData.set(attributeName, offsetAttribute); break; } default: { attributesFormData = attributesFormData.set(attributeName, value); break; } }; break; } default: break; } this.setState({attributesFormData}); this.save({attributesFormData}); } updateProperty(propertyName, value) { let {state: {propertiesFormData}} = this; propertiesFormData = propertiesFormData.setIn([propertyName, 'currentValue'], value); this.setState({propertiesFormData}); this.save({propertiesFormData}); } reset() { this.setState({propertiesFormData: this.initPropData(this.props.element, this.props.layer, this.props.state)}); } save({propertiesFormData, attributesFormData}) { if( propertiesFormData ) { let properties = propertiesFormData.map(data => { return data.get('currentValue'); }); this.context.projectActions.setProperties(properties); } if( attributesFormData ) { switch (this.props.element.prototype) { case 'items': { this.context.projectActions.setItemsAttributes(attributesFormData); break; } case 'lines': { this.context.projectActions.setLinesAttributes(attributesFormData); break; } case 'holes': { this.context.projectActions.setHolesAttributes(attributesFormData); break; } } } } copyProperties( properties ) { this.context.projectActions.copyProperties( properties ); }; pasteProperties() { this.context.projectActions.pasteProperties(); }; render() { let { state: {propertiesFormData, attributesFormData}, context: {projectActions, catalog, translator}, props: {state: appState, element}, } = this; return ( <div> <AttributesEditor element={element} onUpdate={this.updateAttribute} attributeFormData={attributesFormData} state={appState} /> <div style={attrPorpSeparatorStyle}> <div style={headActionStyle}> <div title={translator.t('Copy')} style={iconHeadStyle} onClick={ e => this.copyProperties(element.properties) }><MdContentCopy /></div> { appState.get('clipboardProperties') ? <div title={translator.t('Paste')} style={iconHeadStyle} onClick={ e => this.pasteProperties() }><MdContentPaste /></div> : null } </div> </div> {propertiesFormData.entrySeq() .map(([propertyName, data]) => { let currentValue = data.get('currentValue'), configs = data.get('configs'); let {Editor} = catalog.getPropertyType(configs.type); return <Editor key={propertyName} propertyName={propertyName} value={currentValue} configs={configs} onUpdate={value => this.updateProperty(propertyName, value)} state={appState} sourceElement={element} internalState={this.state} /> }) } </div> ) } } ElementEditor.propTypes = { state: PropTypes.object.isRequired, element: PropTypes.object.isRequired, layer: PropTypes.object.isRequired }; ElementEditor.contextTypes = { projectActions: PropTypes.object.isRequired, catalog: PropTypes.object.isRequired, translator: PropTypes.object.isRequired, };