UNPKG

react-planner

Version:

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

592 lines (456 loc) 21.5 kB
import {Map, List, fromJS} from 'immutable'; import { Group, Layer, Hole, Vertex } from './export'; import { IDBroker, NameGenerator, GeometryUtils, SnapUtils, SnapSceneUtils, } from '../utils/export'; import { MODE_IDLE, MODE_WAITING_DRAWING_LINE, MODE_DRAWING_LINE, MODE_DRAGGING_LINE } from '../constants'; class Line{ static create( state, layerID, type, x0, y0, x1, y1, properties ) { let lineID = IDBroker.acquireID(); let { updatedState: stateV0, vertex: v0 } = Vertex.add( state , layerID, x0, y0, 'lines', lineID ); let { updatedState: stateV1, vertex: v1 } = Vertex.add( stateV0, layerID, x1, y1, 'lines', lineID ); state = stateV1; let line = state.catalog.factoryElement(type, { id: lineID, name: NameGenerator.generateName('lines', state.catalog.getIn(['elements', type, 'info', 'title'])), vertices: new List([v0.id, v1.id]), type }, properties); state = state.setIn(['scene', 'layers', layerID, 'lines', lineID], line); return {updatedState: state, line}; } static select( state, layerID, lineID ){ state = Layer.select( state, layerID ).updatedState; let line = state.getIn([ 'scene','layers', layerID, 'lines', lineID ]); state = Layer.selectElement( state, layerID, 'lines', lineID ).updatedState; state = Layer.selectElement( state, layerID, 'vertices', line.vertices.get(0) ).updatedState; state = Layer.selectElement( state, layerID, 'vertices', line.vertices.get(1) ).updatedState; return {updatedState: state}; } static remove( state, layerID, lineID ) { let line = state.getIn(['scene', 'layers', layerID, 'lines', lineID]); if( line ) { state = this.unselect( state, layerID, lineID ).updatedState; line.holes.forEach(holeID => state = Hole.remove(state, layerID, holeID).updatedState); state = Layer.removeElement( state, layerID, 'lines', lineID ).updatedState; line.vertices.forEach(vertexID => state = Vertex.remove( state, layerID, vertexID, 'lines', lineID ).updatedState); state.getIn(['scene', 'groups']).forEach( group => state = Group.removeElement(state, group.id, layerID, 'lines', lineID).updatedState ); } return {updatedState: state}; } static unselect( state, layerID, lineID ) { let line = state.getIn([ 'scene','layers', layerID, 'lines', lineID ]); if( line ) { state = Layer.unselect( state, layerID, 'vertices', line.vertices.get(0) ).updatedState; state = Layer.unselect( state, layerID, 'vertices', line.vertices.get(1) ).updatedState; state = Layer.unselect( state, layerID, 'lines', lineID ).updatedState; } return {updatedState: state}; } static split( state, layerID, lineID, x, y ) { let line = state.getIn(['scene', 'layers', layerID, 'lines', lineID]); let v0 = state.getIn(['scene', 'layers', layerID, 'vertices', line.vertices.get(0)]); let v1 = state.getIn(['scene', 'layers', layerID, 'vertices', line.vertices.get(1)]); let {x: x0, y: y0} = v0; let {x: x1, y: y1} = v1; let { updatedState: stateL1, line: line0 } = Line.create( state , layerID, line.type, x0, y0, x, y, line.get('properties')); let { updatedState: stateL2, line: line1 } = Line.create( stateL1, layerID, line.type, x1, y1, x, y, line.get('properties')); state = stateL2; let splitPointOffset = GeometryUtils.pointPositionOnLineSegment(x0, y0, x1, y1, x, y); let minVertex = GeometryUtils.minVertex(v0, v1); line.holes.forEach(holeID => { let hole = state.getIn(['scene', 'layers', layerID, 'holes', holeID]); let holeOffset = hole.offset; if (minVertex.x === x1 && minVertex.y === y1) { splitPointOffset = 1 - splitPointOffset; holeOffset = 1 - hole.offset; } if (holeOffset < splitPointOffset) { let offset = holeOffset / splitPointOffset; if (minVertex.x === x1 && minVertex.y === y1) { offset = 1 - offset; } state = Hole.create( state, layerID, hole.type, line0.id, offset, hole.properties ).updatedState; } else { let offset = (holeOffset - splitPointOffset) / (1 - splitPointOffset); if (minVertex.x === x1 && minVertex.y === y1) { offset = 1 - offset; } state = Hole.create( state, layerID, hole.type, line1.id, offset, hole.properties ).updatedState; } }); //add splitted lines to the original line's group let lineGroups = state .getIn(['scene', 'groups']) .filter( group => { const lines = group.getIn(['elements', layerID, 'lines']); return lines && lines.contains(lineID); }); lineGroups.forEach( group => { state = Group.addElement( state, group.id, layerID, 'lines', line0.id ).updatedState; state = Group.addElement( state, group.id, layerID, 'lines', line1.id ).updatedState; }); state = Line.remove( state, layerID, lineID ).updatedState; return { updatedState: state, lines: new List([line0, line1]) }; } static addFromPoints( state, layerID, type, points, properties, holes ) { points = new List(points) .sort(({x: x1, y: y1}, {x: x2, y: y2}) => x1 === x2 ? y1 - y2 : x1 - x2 ); let pointsPair = points.zip(points.skip(1)) .filterNot(([{x: x1, y: y1}, {x: x2, y: y2}]) => x1 === x2 && y1 === y2 ); let lines = []; pointsPair.forEach( ([{x: x1, y: y1}, {x: x2, y: y2}]) => { let { updatedState: stateL, line } = this.create( state, layerID, type, x1, y1, x2, y2, properties ); state = stateL; if( holes ) { holes.forEach(holeWithOffsetPoint => { let {x: xp, y: yp} = holeWithOffsetPoint.offsetPosition; if (GeometryUtils.isPointOnLineSegment(x1, y1, x2, y2, xp, yp)) { let newOffset = GeometryUtils.pointPositionOnLineSegment(x1, y1, x2, y2, xp, yp); if (newOffset >= 0 && newOffset <= 1) { state = Hole.create( state, layerID, holeWithOffsetPoint.hole.type, line.id, newOffset, holeWithOffsetPoint.hole.properties ).updatedState; } } }); } lines.push( line ); }); return { updatedState: state, lines: new List(lines) }; } static createAvoidingIntersections( state, layerID, type, x0, y0, x1, y1, oldProperties, oldHoles ) { let points = [{x: x0, y: y0}, {x: x1, y: y1}]; state = state.getIn(['scene', 'layers', layerID, 'lines']).reduce( ( reducedState, line ) => { let [v0, v1] = line.vertices.map(vertexID => reducedState.getIn(['scene', 'layers', layerID, 'vertices']).get(vertexID)).toArray(); let hasCommonEndpoint = ( GeometryUtils.samePoints(v0, points[0]) || GeometryUtils.samePoints(v0, points[1]) || GeometryUtils.samePoints(v1, points[0]) || GeometryUtils.samePoints(v1, points[1]) ); let intersection = GeometryUtils.twoLineSegmentsIntersection( points[0], points[1], v0, v1 ); if (intersection.type === 'colinear') { if (!oldHoles) { oldHoles = []; } let orderedVertices = GeometryUtils.orderVertices(points); reducedState.getIn(['scene', 'layers', layerID, 'lines', line.id, 'holes']).forEach(holeID => { let hole = reducedState.getIn(['scene', 'layers', layerID, 'holes', holeID]); let oldLineLength = GeometryUtils.pointsDistance(v0.x, v0.y, v1.x, v1.y); let offset = GeometryUtils.samePoints( orderedVertices[1], line.vertices.get(1) ) ? ( 1 - hole.offset ) : hole.offset; let offsetPosition = GeometryUtils.extendLine( v0.x, v0.y, v1.x, v1.y, oldLineLength * offset ); oldHoles.push({hole, offsetPosition}); }); reducedState = this.remove( reducedState, layerID, line.id ).updatedState; points.push(v0, v1); } if (intersection.type === 'intersecting' && (!hasCommonEndpoint)) { reducedState = this.split( reducedState, layerID, line.id, intersection.point.x, intersection.point.y ).updatedState; points.push(intersection.point); } return reducedState; }, state ); let { updatedState, lines } = Line.addFromPoints( state, layerID, type, points, oldProperties, oldHoles ); return { updatedState, lines }; } static replaceVertex ( state, layerID, lineID, vertexIndex, x, y ) { let vertexID = state.getIn(['scene', 'layers', layerID, 'lines', lineID, 'vertices', vertexIndex]); state = Vertex.remove( state, layerID, vertexID, 'lines', lineID ).updatedState; let { updatedState: stateV, vertex } = Vertex.add( state, layerID, x, y, 'lines', lineID ); state = stateV; state = state.setIn(['scene', 'layers', layerID, 'lines', lineID, 'vertices', vertexIndex], vertex.id); state = state.setIn(['scene', 'layers', layerID, 'lines', lineID], state.getIn(['scene', 'layers', layerID, 'lines', lineID]) ); return { updatedState: state, line: state.getIn(['scene', 'layers', layerID, 'lines', lineID]), vertex }; } static selectToolDrawingLine(state, sceneComponentType) { state = state.merge({ mode: MODE_WAITING_DRAWING_LINE, drawingSupport: new Map({ type: sceneComponentType }) }); return { updatedState: state }; } static beginDrawingLine(state, layerID, x, y) { let snapElements = SnapSceneUtils.sceneSnapElements(state.scene, new List(), state.snapMask); let snap = null; if (state.snapMask && !state.snapMask.isEmpty()) { snap = SnapUtils.nearestSnap(snapElements, x, y, state.snapMask); if (snap) ({x, y} = snap.point); snapElements = snapElements.withMutations(snapElements => { let a, b, c; ({a, b, c} = GeometryUtils.horizontalLine(y)); SnapUtils.addLineSnap(snapElements, a, b, c, 10, 3, null); ({a, b, c} = GeometryUtils.verticalLine(x)); SnapUtils.addLineSnap(snapElements, a, b, c, 10, 3, null); }); } let drawingSupport = state.get('drawingSupport').set('layerID', layerID); state = Layer.unselectAll( state, layerID ).updatedState; let { updatedState: stateL, line } = Line.create( state, layerID, drawingSupport.get('type'), x, y, x, y ); state = Line.select( stateL, layerID, line.id ).updatedState; state = state.merge({ mode: MODE_DRAWING_LINE, snapElements, activeSnapElement: snap ? snap.snap : null, drawingSupport }); return { updatedState: state }; } static updateDrawingLine(state, x, y) { let snap = null; if (state.snapMask && !state.snapMask.isEmpty()) { snap = SnapUtils.nearestSnap(state.snapElements, x, y, state.snapMask); if (snap) ({x, y} = snap.point); } let layerID = state.getIn(['drawingSupport', 'layerID']); let lineID = state.getIn(['scene', 'layers', layerID, 'selected', 'lines']).first(); let { updatedState: stateLV, vertex } = Line.replaceVertex( state, layerID, lineID, 1, x, y ); state = stateLV; state = this.select( state, layerID, lineID ).updatedState; state = state.merge({ activeSnapElement: snap ? snap.snap : null }); return { updatedState: state }; } static endDrawingLine(state, x, y) { if (state.snapMask && !state.snapMask.isEmpty()) { let snap = SnapUtils.nearestSnap(state.snapElements, x, y, state.snapMask); if (snap) ({x, y} = snap.point); } let layerID = state.getIn(['drawingSupport', 'layerID']); let layer = state.getIn(['scene','layers', layerID]); let lineID = state.getIn(['scene', 'layers', layerID, 'selected', 'lines']).first(); let line = state.getIn(['scene', 'layers', layerID, 'lines', lineID]); let v0 = layer.vertices.get(line.vertices.get(0)); state = Line.remove( state, layerID, lineID ).updatedState; state = Line.createAvoidingIntersections( state, layerID, line.type, v0.x, v0.y, x, y ).updatedState; state = Layer.detectAndUpdateAreas( state, layerID ).updatedState; state = state.merge({ mode: MODE_WAITING_DRAWING_LINE, snapElements: new List(), activeSnapElement: null }); return { updatedState: state }; } static beginDraggingLine(state, layerID, lineID, x, y) { let snapElements = SnapSceneUtils.sceneSnapElements(state.scene, new List(), state.snapMask); let layer = state.scene.layers.get(layerID); let line = layer.lines.get(lineID); let vertex0 = layer.vertices.get(line.vertices.get(0)); let vertex1 = layer.vertices.get(line.vertices.get(1)); state = state.merge({ mode: MODE_DRAGGING_LINE, snapElements, draggingSupport: Map({ layerID, lineID, startPointX: x, startPointY: y, startVertex0X: vertex0.x, startVertex0Y: vertex0.y, startVertex1X: vertex1.x, startVertex1Y: vertex1.y, }) }); return { updatedState: state }; } static updateDraggingLine(state, x, y) { let draggingSupport = state.draggingSupport; let snapElements = state.snapElements; let layerID = draggingSupport.get('layerID'); let lineID = draggingSupport.get('lineID'); let diffX = x - draggingSupport.get('startPointX'); let diffY = y - draggingSupport.get('startPointY'); let newVertex0X = draggingSupport.get('startVertex0X') + diffX; let newVertex0Y = draggingSupport.get('startVertex0Y') + diffY; let newVertex1X = draggingSupport.get('startVertex1X') + diffX; let newVertex1Y = draggingSupport.get('startVertex1Y') + diffY; let activeSnapElement = null; let curSnap0 = null, curSnap1 = null; if (state.snapMask && !state.snapMask.isEmpty()) { curSnap0 = SnapUtils.nearestSnap(snapElements, newVertex0X, newVertex0Y, state.snapMask); curSnap1 = SnapUtils.nearestSnap(snapElements, newVertex1X, newVertex1Y, state.snapMask); } let deltaX = 0, deltaY = 0; if (curSnap0 && curSnap1) { if (curSnap0.point.distance < curSnap1.point.distance) { deltaX = curSnap0.point.x - newVertex0X; deltaY = curSnap0.point.y - newVertex0Y; activeSnapElement = curSnap0.snap; } else { deltaX = curSnap1.point.x - newVertex1X; deltaY = curSnap1.point.y - newVertex1Y; activeSnapElement = curSnap1.snap; } } else { if (curSnap0) { deltaX = curSnap0.point.x - newVertex0X; deltaY = curSnap0.point.y - newVertex0Y; activeSnapElement = curSnap0.snap; } if (curSnap1) { deltaX = curSnap1.point.x - newVertex1X; deltaY = curSnap1.point.y - newVertex1Y; activeSnapElement = curSnap1.snap; } } newVertex0X += deltaX; newVertex0Y += deltaY; newVertex1X += deltaX; newVertex1Y += deltaY; state = state.merge({ activeSnapElement, scene: state.scene.updateIn(['layers', layerID], layer => layer.withMutations(layer => { let lineVertices = layer.getIn(['lines', lineID, 'vertices']); layer.updateIn(['vertices', lineVertices.get(0)], vertex => vertex.merge({x: newVertex0X, y: newVertex0Y})); layer.updateIn(['vertices', lineVertices.get(1)], vertex => vertex.merge({x: newVertex1X, y: newVertex1Y})); return layer; })) }); return { updatedState: state }; } static endDraggingLine(state, x, y) { let {draggingSupport} = state; let layerID = draggingSupport.get('layerID'); let layer = state.scene.layers.get(layerID); let lineID = draggingSupport.get('lineID'); let line = layer.lines.get(lineID); let vertex0 = layer.vertices.get(line.vertices.get(0)); let vertex1 = layer.vertices.get(line.vertices.get(1)); let maxV = GeometryUtils.maxVertex(vertex0, vertex1); let minV = GeometryUtils.minVertex(vertex0, vertex1); let lineLength = GeometryUtils.verticesDistance(minV,maxV); let alpha = Math.atan2(maxV.y - minV.y, maxV.x - minV.x); let holesWithOffsetPosition = []; layer.lines.get(lineID).holes.forEach(holeID => { let hole = layer.holes.get(holeID); let pointOnLine = lineLength * hole.offset; let offsetPosition = { x: pointOnLine * Math.cos(alpha) + minV.x, y: pointOnLine * Math.sin(alpha) + minV.y }; holesWithOffsetPosition.push({hole, offsetPosition}); }); let diffX = x - draggingSupport.get('startPointX'); let diffY = y - draggingSupport.get('startPointY'); let newVertex0X = draggingSupport.get('startVertex0X') + diffX; let newVertex0Y = draggingSupport.get('startVertex0Y') + diffY; let newVertex1X = draggingSupport.get('startVertex1X') + diffX; let newVertex1Y = draggingSupport.get('startVertex1Y') + diffY; if (state.snapMask && !state.snapMask.isEmpty()) { let curSnap0 = SnapUtils.nearestSnap(state.snapElements, newVertex0X, newVertex0Y, state.snapMask); let curSnap1 = SnapUtils.nearestSnap(state.snapElements, newVertex1X, newVertex1Y, state.snapMask); let deltaX = 0, deltaY = 0; if (curSnap0 && curSnap1) { if (curSnap0.point.distance < curSnap1.point.distance) { deltaX = curSnap0.point.x - newVertex0X; deltaY = curSnap0.point.y - newVertex0Y; } else { deltaX = curSnap1.point.x - newVertex1X; deltaY = curSnap1.point.y - newVertex1Y; } } else { if (curSnap0) { deltaX = curSnap0.point.x - newVertex0X; deltaY = curSnap0.point.y - newVertex0Y; } if (curSnap1) { deltaX = curSnap1.point.x - newVertex1X; deltaY = curSnap1.point.y - newVertex1Y; } } newVertex0X += deltaX; newVertex0Y += deltaY; newVertex1X += deltaX; newVertex1Y += deltaY; } let lineGroups = state //get groups membership if present .getIn(['scene', 'groups']) .filter( group => { const lines = group.getIn(['elements', layerID, 'lines']); return lines && lines.contains(lineID); }); state = Layer.mergeEqualsVertices( state, layerID, line.vertices.get(0) ).updatedState; state = Layer.mergeEqualsVertices( state, layerID, line.vertices.get(1) ).updatedState; state = Line.remove( state, layerID, lineID ).updatedState; if(!GeometryUtils.samePoints({newVertex0X, newVertex0Y}, {newVertex1X, newVertex1Y})) { let ret = Line.createAvoidingIntersections( state, layerID, line.type, newVertex0X, newVertex0Y, newVertex1X, newVertex1Y, line.properties, holesWithOffsetPosition ); state = ret.updatedState; //re-add to old line's groups if present ret.lines.forEach( addedLine => { lineGroups.forEach( oldLineGroup => { state = Group.addElement( state, oldLineGroup.id, layerID, 'lines', addedLine.id ).updatedState; }); }); } state = Layer.detectAndUpdateAreas( state, layerID ).updatedState; state = state.merge({ mode: MODE_IDLE, draggingSupport: null, activeSnapElement: null, snapElements: new List() }); return { updatedState: state }; } static setProperties( state, layerID, lineID, properties ) { state = state.mergeIn(['scene', 'layers', layerID, 'lines', lineID, 'properties'], properties); return { updatedState: state }; } static setJsProperties( state, layerID, lineID, properties ) { return this.setProperties( state, layerID, lineID, fromJS(properties) ); } static updateProperties( state, layerID, lineID, properties) { properties.forEach( ( v, k ) => { if( state.hasIn(['scene', 'layers', layerID, 'lines', lineID, 'properties', k]) ) state = state.mergeIn(['scene', 'layers', layerID, 'lines', lineID, 'properties', k], v); }); return { updatedState: state }; } static updateJsProperties( state, layerID, lineID, properties) { return this.updateProperties( state, layerID, lineID, fromJS(properties) ); } static setAttributes( state, layerID, lineID, lineAttributes ) { let lAttr = lineAttributes.toJS(); let {vertexOne, vertexTwo, lineLength} = lAttr; delete lAttr['vertexOne']; delete lAttr['vertexTwo']; delete lAttr['lineLength']; state = state .mergeIn(['scene', 'layers', layerID, 'lines', lineID], fromJS(lAttr)) .mergeIn(['scene', 'layers', layerID, 'vertices', vertexOne.id], {x: vertexOne.x, y: vertexOne.y}) .mergeIn(['scene', 'layers', layerID, 'vertices', vertexTwo.id], {x: vertexTwo.x, y: vertexTwo.y}) .mergeIn(['scene', 'layers', layerID, 'lines', lineID, 'misc'], new Map({'_unitLength': lineLength._unit})); state = Layer.mergeEqualsVertices( state, layerID, vertexOne.id ).updatedState; if (vertexOne.x != vertexTwo.x && vertexOne.y != vertexTwo.y) { state = Layer.mergeEqualsVertices( state, layerID, vertexTwo.id ).updatedState; } state = Layer.detectAndUpdateAreas( state, layerID ).updatedState; return { updatedState: state }; } static setVerticesCoords( state, layerID, lineID, x1, y1, x2, y2 ) { let line = state.getIn(['scene', 'layers', layerID, 'lines', lineID]); state = Vertex.setAttributes( state, layerID, line.vertices.get(0), new Map({ x: x1, y: y1 }) ).updatedState; state = Vertex.setAttributes( state, layerID, line.vertices.get(1), new Map({ x: x2, y: y2 }) ).updatedState; return { updatedState: state }; } } export { Line as default };