UNPKG

react-floorplanner

Version:

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

655 lines (518 loc) 21.2 kB
/** lines features **/ import {Map, List, fromJS} from 'immutable'; import {Vertex} from '../models'; import IDBroker from './id-broker'; import NameGenerator from './name-generator'; import * as Geometry from './geometry'; import calculateInnerCyles, {isClockWiseOrder} from './graph-inner-cycles'; const flatten = list => list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); export function addLine(layer, type, x0, y0, x1, y1, catalog, properties = {}) { let line; layer = layer.withMutations(layer => { let lineID = IDBroker.acquireID(); let v0, v1; ({layer, vertex: v0} = addVertex(layer, x0, y0, 'lines', lineID)); ({layer, vertex: v1} = addVertex(layer, x1, y1, 'lines', lineID)); line = catalog.factoryElement(type, { id: lineID, name: NameGenerator.generateName('lines', catalog.getIn(['elements', type, 'info', 'title'])), vertices: new List([v0.id, v1.id]), type }, properties); layer.setIn(['lines', lineID], line); }); return {layer, line}; } export function replaceLineVertex(layer, lineID, vertexIndex, x, y) { let line = layer.getIn(['lines', lineID]); let vertex; layer = layer.withMutations(layer => layer.withMutations(layer => { let vertexID = line.vertices.get(vertexIndex); unselect(layer, 'vertices', vertexID); removeVertex(layer, vertexID, 'lines', line.id); ({layer, vertex} = addVertex(layer, x, y, 'lines', line.id)); line = line.setIn(['vertices', vertexIndex], vertex.id); layer.setIn(['lines', lineID], line); })); return {layer, line, vertex}; } export function removeLine(layer, lineID) { let line = layer.getIn(['lines', lineID]); layer = layer.withMutations(layer => { unselect(layer, 'lines', lineID); line.holes.forEach(holeID => removeHole(layer, holeID)); layer.deleteIn(['lines', line.id]); line.vertices.forEach(vertexID => removeVertex(layer, vertexID, 'lines', line.id)); }); return {layer, line}; } export function splitLine(layer, lineID, x, y, catalog) { let line0, line1; layer = layer.withMutations(layer => { let line = layer.getIn(['lines', lineID]); let v0 = layer.vertices.get(line.vertices.get(0)); let v1 = layer.vertices.get(line.vertices.get(1)); let {x: x0, y: y0} = v0; let {x: x1, y: y1} = v1; ({line: line0} = addLine(layer, line.type, x0, y0, x, y, catalog, line.properties)); ({line: line1} = addLine(layer, line.type, x1, y1, x, y, catalog, line.properties)); let splitPointOffset = Geometry.pointPositionOnLineSegment(x0, y0, x1, y1, x, y); let minVertex = Geometry.minVertex(v0, v1); line.holes.forEach(holeID => { let hole = layer.holes.get(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; } addHole(layer, hole.type, line0.id, offset, catalog, hole.properties); } else { let offset = (holeOffset - splitPointOffset) / (1 - splitPointOffset); if (minVertex.x === x1 && minVertex.y === y1) { offset = 1 - offset; } addHole(layer, hole.type, line1.id, offset, catalog, hole.properties); } }); removeLine(layer, lineID); }); return {layer, lines: new List([line0, line1])}; } export function addLinesFromPoints(layer, type, points, catalog, properties, holes) { points = new List(points) .sort(({x: x1, y: y1}, {x: x2, y: y2}) => { return x1 === x2 ? y1 - y2 : x1 - x2; }); let pointsPair = points.zip(points.skip(1)) .filterNot(([{x: x1, y: y1}, {x: x2, y: y2}]) => { return x1 === x2 && y1 === y2; }); let lines = (new List()).withMutations(lines => { layer = layer.withMutations(layer => { pointsPair.forEach(([{x: x1, y: y1}, {x: x2, y: y2}]) => { let {line} = addLine(layer, type, x1, y1, x2, y2, catalog, properties); if (holes) { holes.forEach(holeWithOffsetPoint => { let {x: xp, y: yp} = holeWithOffsetPoint.offsetPosition; if (Geometry.isPointOnLineSegment(x1, y1, x2, y2, xp, yp)) { let newOffset = Geometry.pointPositionOnLineSegment(x1, y1, x2, y2, xp, yp); if (newOffset >= 0 && newOffset <= 1) { addHole(layer, holeWithOffsetPoint.hole.type, line.id, newOffset, catalog, holeWithOffsetPoint.hole.properties); } } }); } lines.push(line); }); }); }); return {layer, lines}; } export function addLineAvoidingIntersections(layer, type, x0, y0, x1, y1, catalog, oldProperties, oldHoles) { let points = [{x: x0, y: y0}, {x: x1, y: y1}]; layer = layer.withMutations(layer => { let {lines, vertices} = layer; lines.forEach(line => { let [v0, v1] = line.vertices.map(vertexID => vertices.get(vertexID)).toArray(); let hasCommonEndpoint = (Geometry.samePoints(v0, points[0]) || Geometry.samePoints(v0, points[1]) || Geometry.samePoints(v1, points[0]) || Geometry.samePoints(v1, points[1])); let intersection = Geometry.intersectionFromTwoLineSegment( points[0], points[1], v0, v1 ); if (intersection.type === 'colinear') { if (!oldHoles) { oldHoles = []; } let orderedVertices = Geometry.orderVertices(points); layer.lines.get(line.id).holes.forEach(holeID => { let hole = layer.holes.get(holeID); let oldLineLength = Geometry.pointsDistance(v0.x, v0.y, v1.x, v1.y); let alpha = Math.atan2(orderedVertices[1].y - orderedVertices[0].y, orderedVertices[1].x - orderedVertices[0].x); let offset = hole.offset; if (orderedVertices[1].x === line.vertices.get(1).x && orderedVertices[1].y === line.vertices(1).y) { offset = 1 - offset; } let xp = oldLineLength * offset * Math.cos(alpha) + v0.x; let yp = oldLineLength * offset * Math.sin(alpha) + v0.y; oldHoles.push({hole, offsetPosition: {x: xp, y: yp}}); }); removeLine(layer, line.id); points.push(v0, v1); } if (intersection.type === 'intersecting' && (!hasCommonEndpoint)) { splitLine(layer, line.id, intersection.point.x, intersection.point.y, catalog); points.push(intersection.point); } }); addLinesFromPoints(layer, type, points, catalog, oldProperties, oldHoles); }); return {layer}; } /** vertices features **/ export function addVertex(layer, x, y, relatedPrototype, relatedID) { let vertex = layer.vertices.find(vertex => Geometry.samePoints(vertex, {x, y})); if (vertex) { vertex = vertex.update(relatedPrototype, related => related.push(relatedID)); } else { vertex = new Vertex({ id: IDBroker.acquireID(), name: 'Vertex', x, y, [relatedPrototype]: new List([relatedID]) }); } layer = layer.setIn(['vertices', vertex.id], vertex); return {layer, vertex}; } export function removeVertex(layer, vertexID, relatedPrototype, relatedID) { let vertex = layer.vertices.get(vertexID); vertex = vertex.update(relatedPrototype, related => { let index = related.findIndex(ID => relatedID === ID); return related.delete(index); }); layer = vertex.areas.size || vertex.lines.size ? layer.setIn(['vertices', vertex.id], vertex) : layer.deleteIn(['vertices', vertex.id]); return {layer, vertex}; } export function mergeEqualsVertices(layer, vertexID) { //1. find vertices to remove let vertex = layer.getIn(['vertices', vertexID]); let doubleVertices = layer.vertices .filter(v => v.id !== vertexID && Geometry.samePoints(vertex, v)); if (doubleVertices.isEmpty()) return layer; //2. remove double vertices let vertices, lines, areas; vertices = layer.vertices.withMutations(vertices => { lines = layer.lines.withMutations(lines => { areas = layer.areas.withMutations(areas => { doubleVertices.forEach(doubleVertex => { doubleVertex.lines.forEach(lineID => { let line = lines.get(lineID); line = line.update('vertices', vertices => vertices.map(v => v === doubleVertex.id ? vertexID : v)); lines.set(lineID, line); vertices.updateIn([vertexID, 'lines'], l => l.push(lineID)); }); doubleVertex.areas.forEach(areaID => { let area = areas.get(areaID); area = area.update('vertices', vertices => vertices.map(v => v === doubleVertex.id ? vertexID : v)); areas.set(areaID, area); vertices.updateIn([vertexID, 'areas'], area => area.push(areaID)); }); vertices.remove(doubleVertex.id); }); }); }); }); //3. update layer return layer.merge({ vertices, lines, areas }); } export function select(layer, prototype, ID) { return layer.withMutations(layer => { layer.setIn([prototype, ID, 'selected'], true); layer.updateIn(['selected', prototype], elements => elements.push(ID)); }); } export function unselect(layer, prototype, ID) { return layer.withMutations(layer => { let ids = layer.getIn(['selected', prototype]); ids = ids.remove(ids.indexOf(ID)); let selected = ids.some(key => key === ID); layer.setIn(['selected', prototype], ids); layer.setIn([prototype, ID, 'selected'], selected); }); } function opSetProperties(layer, prototype, ID, properties) { properties = fromJS(properties); layer.mergeIn([prototype, ID, 'properties'], properties); } function opUpdateProperties(layer, prototype, ID, properties) { fromJS(properties).forEach( ( v, k ) => { if( layer.hasIn([prototype, ID, 'properties', k]) ) layer.mergeIn([prototype, ID, 'properties', k], v); }); } function opSetItemsAttributes(layer, prototype, ID, itemsAttributes) { itemsAttributes = fromJS(itemsAttributes); layer.mergeIn([prototype, ID], itemsAttributes); } function opSetLinesAttributes(layer, prototype, ID, linesAttributes, catalog) { let lAttr = linesAttributes.toJS(); let {vertexOne, vertexTwo, lineLength} = lAttr; delete lAttr['vertexOne']; delete lAttr['vertexTwo']; delete lAttr['lineLength']; layer = layer .mergeIn([prototype, ID], fromJS(lAttr)) //all the others attributes .mergeIn(['vertices', vertexOne.id], {x: vertexOne.x, y: vertexOne.y}) .mergeIn(['vertices', vertexTwo.id], {x: vertexTwo.x, y: vertexTwo.y}) .mergeDeepIn([prototype, ID, 'misc'], new Map({'_unitLength': lineLength._unit})); layer = mergeEqualsVertices(layer, vertexOne.id); //check if second vertex has different coordinates than the first if (vertexOne.x != vertexTwo.x && vertexOne.y != vertexTwo.y) layer = mergeEqualsVertices(layer, vertexTwo.id); detectAndUpdateAreas(layer, catalog); } function opSetHolesAttributes(layer, prototype, ID, holesAttributes) { let hAttr = holesAttributes.toJS(); let {offsetA, offsetB, offset} = hAttr; delete hAttr['offsetA']; delete hAttr['offsetB']; delete hAttr['offset']; let misc = new Map({_unitA: offsetA._unit, _unitB: offsetB._unit}); layer .mergeIn([prototype, ID], fromJS(hAttr)) //all the others attributes .mergeDeepIn([prototype, ID], new Map({offset, misc})); } export function setPropertiesOnSelected(layer, properties) { return layer.withMutations(layer => { let selected = layer.selected; selected.lines.forEach(lineID => opSetProperties(layer, 'lines', lineID, properties)); selected.holes.forEach(holeID => opSetProperties(layer, 'holes', holeID, properties)); selected.areas.forEach(areaID => opSetProperties(layer, 'areas', areaID, properties)); selected.items.forEach(itemID => opSetProperties(layer, 'items', itemID, properties)); }); } export function updatePropertiesOnSelected(layer, properties) { return layer.withMutations(layer => { let selected = layer.selected; selected.lines.forEach(lineID => opUpdateProperties(layer, 'lines', lineID, properties)); selected.holes.forEach(holeID => opUpdateProperties(layer, 'holes', holeID, properties)); selected.areas.forEach(areaID => opUpdateProperties(layer, 'areas', areaID, properties)); selected.items.forEach(itemID => opUpdateProperties(layer, 'items', itemID, properties)); }); } export function setAttributesOnSelected(layer, attributes, catalog) { return layer.withMutations(layer => { let selected = layer.selected; selected.lines.forEach(lineID => opSetLinesAttributes(layer, 'lines', lineID, attributes, catalog)); selected.holes.forEach(holeID => opSetHolesAttributes(layer, 'holes', holeID, attributes, catalog)); selected.items.forEach(itemID => opSetItemsAttributes(layer, 'items', itemID, attributes, catalog)); //selected.areas.forEach(areaID => opSetItemsAttributes(layer, 'areas', areaID, attributes, catalog)); }); } export function unselectAll(layer) { let selected = layer.get('selected'); return layer.withMutations(layer => { layer.selected.forEach((ids, prototype) => { ids.forEach(id => unselect(layer, prototype, id)); }); }); } /** areas features **/ export function addArea(layer, type, verticesCoords, catalog) { let area; layer = layer.withMutations(layer => { let areaID = IDBroker.acquireID(); let vertices = verticesCoords.map( ( v ) => addVertex(layer, v.x, v.y, 'areas', areaID).vertex.id ); area = catalog.factoryElement(type, { id: areaID, name: NameGenerator.generateName('areas', catalog.getIn(['elements', type, 'info', 'title'])), type, prototype: 'areas', vertices }); layer.setIn(['areas', areaID], area); }); return {layer, area}; } export function removeArea(layer, areaID) { let area = layer.getIn(['areas', areaID]); layer = layer.withMutations(layer => { unselect(layer, 'areas', areaID); layer.deleteIn(['areas', area.id]); area.vertices.forEach(vertexID => removeVertex(layer, vertexID, 'areas', area.id)); }); return {layer, area}; } const sameSet = (set1, set2) => set1.size === set2.size && set1.isSuperset(set2) && set1.isSubset(set2); //https://github.com/MartyWallace/PolyK function ContainsPoint(polygon, pointX, pointY) { let n = polygon.length >> 1; let ax, lup; let ay = polygon[2 * n - 3] - pointY; let bx = polygon[2 * n - 2] - pointX; let by = polygon[2 * n - 1] - pointY; if (bx === 0 && by === 0) return false; // point on edge // let lup = by > ay; for (let ii = 0; ii < n; ii++) { ax = bx; ay = by; bx = polygon[2 * ii] - pointX; by = polygon[2 * ii + 1] - pointY; if (bx === 0 && by === 0) return false; // point on edge if (ay === by) continue; lup = by > ay; } let depth = 0; for (let i = 0; i < n; i++) { ax = bx; ay = by; bx = polygon[2 * i] - pointX; by = polygon[2 * i + 1] - pointY; if (ay < 0 && by < 0) continue; // both 'up' or both 'down' if (ay > 0 && by > 0) continue; // both 'up' or both 'down' if (ax < 0 && bx < 0) continue; // both points on the left if (ay === by && Math.min(ax, bx) < 0) return true; if (ay === by) continue; let lx = ax + (bx - ax) * (-ay) / (by - ay); if (lx === 0) return false; // point on edge if (lx > 0) depth++; if (ay === 0 && lup && by > ay) depth--; // hit vertex, both up if (ay === 0 && !lup && by < ay) depth--; // hit vertex, both down lup = by > ay; } return (depth & 1) === 1; } export function detectAndUpdateAreas(layer, catalog) { let verticesArray = []; //array with vertices coords let linesArray; //array with edges let vertexID_to_verticesArrayIndex = {}; let verticesArrayIndex_to_vertexID = {}; layer.vertices.forEach(vertex => { let verticesCount = verticesArray.push([vertex.x, vertex.y]); let latestVertexIndex = verticesCount - 1; vertexID_to_verticesArrayIndex[vertex.id] = latestVertexIndex; verticesArrayIndex_to_vertexID[latestVertexIndex] = vertex.id; }); linesArray = layer.lines.map(line => line.vertices.map(vertexID => vertexID_to_verticesArrayIndex[vertexID]).toArray()); let innerCyclesByVerticesArrayIndex = calculateInnerCyles(verticesArray, linesArray); let innerCyclesByVerticesID = innerCyclesByVerticesArrayIndex .map(cycle => cycle.map(vertexIndex => verticesArrayIndex_to_vertexID[vertexIndex])); // All area vertices should be ordered in counterclockwise order innerCyclesByVerticesID = innerCyclesByVerticesID.map( ( area ) => isClockWiseOrder( area.map(vertexID => layer.vertices.get(vertexID) ) ) ? area.reverse() : area ); let areaIDs = []; layer = layer.withMutations(layer => { //remove areas layer.areas.forEach(area => { let areaInUse = innerCyclesByVerticesID.some(vertices => sameSet(vertices, area.vertices)); if (!areaInUse) removeArea(layer, area.id); }); //add new areas innerCyclesByVerticesID.forEach((cycle, ind) => { let areaInUse = layer.areas.find(area => sameSet(area.vertices, cycle)); if (areaInUse) { areaIDs[ind] = areaInUse.id; layer.setIn(['areas', areaIDs[ind], 'holes'], new List()); } else { let areaVerticesCoords = cycle.map(vertexId => layer.vertices.get(vertexId)); let {area} = addArea(layer, 'area', areaVerticesCoords, catalog); areaIDs[ind] = area.id; } }); // Build a relationship between areas and their coordinates let verticesCoordsForArea = areaIDs.map(id => { let vertices = layer.areas.get(id).vertices.map(vertexID => layer.vertices.get(vertexID)); return { id, vertices }; }); // Find all holes for an area let i, j; for (i = 0; i < verticesCoordsForArea.length; i++) { let holesList = new List(); // The holes for this area let areaVerticesList = verticesCoordsForArea[i].vertices.flatten().toArray(); for (j = 0; j < verticesCoordsForArea.length; j++) { if (i !== j) { let isHole = ContainsPoint(areaVerticesList, verticesCoordsForArea[j].vertices.get(0).get(0), verticesCoordsForArea[j].vertices.get(0).get(1)); if (isHole) { holesList = holesList.push(verticesCoordsForArea[j].id); } } } layer.setIn(['areas', verticesCoordsForArea[i].id, 'holes'], holesList); } // Remove holes which are already holes for other areas areaIDs.forEach(areaID => { let doubleHoles = new Set(); let areaHoles = layer.getIn(['areas', areaID, 'holes']); areaHoles.forEach((areaHoleID) => { let holesOfholes = layer.getIn(['areas', areaHoleID, 'holes']); holesOfholes.forEach((holeID) => { let holeIndex = areaHoles.indexOf(holeID); if (holeIndex !== -1) { doubleHoles.add(holeID); } }); }); doubleHoles.forEach(doubleHoleID => { let holeIndex = areaHoles.indexOf(doubleHoleID); areaHoles = areaHoles.remove(holeIndex); }); layer.setIn(['areas', areaID, 'holes'], areaHoles); }); }); return {layer}; } /** holes features **/ export function addHole(layer, type, lineID, offset, catalog, properties = {}) { let hole; layer = layer.withMutations(layer => { let holeID = IDBroker.acquireID(); hole = catalog.factoryElement(type, { id: holeID, name: NameGenerator.generateName('holes', catalog.getIn(['elements', type, 'info', 'title'])), type, offset, line: lineID }, properties); layer.setIn(['holes', holeID], hole); layer.updateIn(['lines', lineID, 'holes'], holes => holes.push(holeID)); }); return {layer, hole}; } export function removeHole(layer, holeID) { let hole = layer.getIn(['holes', holeID]); layer = layer.withMutations(layer => { unselect(layer, 'holes', holeID); layer.deleteIn(['holes', hole.id]); layer.updateIn(['lines', hole.line, 'holes'], holes => { let index = holes.findIndex(ID => holeID === ID); return holes.remove(index); }); }); return {layer, hole}; } /** items features **/ export function addItem(layer, type, x, y, width, height, rotation, catalog) { let item; layer = layer.withMutations(layer => { let itemID = IDBroker.acquireID(); item = catalog.factoryElement(type, { id: itemID, name: NameGenerator.generateName('items', catalog.getIn(['elements', type, 'info', 'title'])), type, height, width, x, y, rotation }); layer.setIn(['items', itemID], item); }); return {layer, item}; } export function removeItem(layer, itemID) { let item = layer.getIn(['items', itemID]); layer = layer.withMutations(layer => { unselect(layer, 'items', itemID); layer.deleteIn(['items', item.id]); }); return {layer, item}; }