UNPKG

react-floorplanner

Version:

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

628 lines (516 loc) 19.9 kB
import * as Three from 'three'; import createGrid from './grid-creator'; import {disposeObject} from './three-memory-cleaner'; export function parseData(sceneData, actions, catalog) { let planData = {}; planData.sceneGraph = { unit: sceneData.unit, layers: {}, width: sceneData.width, height: sceneData.height, LODs: {} }; planData.plan = new Three.Object3D(); // Add a grid to the plan planData.grid = createGrid(sceneData); planData.boundingBox = new Three.Box3().setFromObject(planData.grid); let promises = []; sceneData.layers.forEach(layer => { if (layer.id === sceneData.selectedLayer || layer.visible) { promises = promises.concat(createLayerObjects(layer, planData, sceneData, actions, catalog)); } }); Promise.all(promises).then(value => { updateBoundingBox(planData); }); return planData; } function createLayerObjects(layer, planData, sceneData, actions, catalog) { let promises = []; planData.sceneGraph.layers[layer.id] = { id: layer.id, lines: {}, holes: {}, areas: {}, items: {}, visible: layer.visible, altitude: layer.altitude }; // Import lines layer.lines.forEach(line => { promises.push(addLine(sceneData, planData, layer, line.id, catalog, actions.linesActions)); line.holes.forEach(holeID => { promises.push(addHole(sceneData, planData, layer, holeID, catalog, actions.holesActions)); }) }); // Import areas layer.areas.forEach(area => { promises.push(addArea(sceneData, planData, layer, area.id, catalog, actions.areaActions)); }); // Import items layer.items.forEach(item => { promises.push(addItem(sceneData, planData, layer, item.id, catalog, actions.itemsActions)); }); return promises; } export function updateScene(planData, sceneData, oldSceneData, diffArray, actions, catalog) { filterDiffs(diffArray, sceneData, oldSceneData).forEach(diff => { /* First of all I need to find the object I need to update */ let modifiedPath = diff.path.split("/"); if (modifiedPath[1] === "layers") { let layer = sceneData[modifiedPath[1]].get(modifiedPath[2]); if (modifiedPath.length === 3) { switch (diff.op) { case 'replace': break; //TODO? case 'add': break; //TODO? case 'remove': removeLayer(modifiedPath[2], planData); break; } } else if (modifiedPath.length > 3) { switch (diff.op) { case 'replace': replaceObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog); break; case 'add': addObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog); break; case 'remove': removeObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog); break; } } } else if (modifiedPath[1] === 'selectedLayer') { let layerSelectedID = diff.value; // First of all I check if the new selected layer is not visible if (!sceneData.layers.get(layerSelectedID).visible) { // I need to create the objects for this layer let promises = createLayerObjects(sceneData.layers.get(layerSelectedID), planData, sceneData, actions, catalog); Promise.all(promises).then(values => { updateBoundingBox(planData); }) } let layerGraph = planData.sceneGraph.layers[oldSceneData.selectedLayer]; if (layerGraph) { if (!layerGraph.visible) { // I need to remove the objects for this layer for (let lineID in layerGraph.lines) removeLine(planData, layerGraph.id, lineID); for (let areaID in layerGraph.areas) removeArea(planData, layerGraph.id, areaID); for (let itemID in layerGraph.items) removeItem(planData, layerGraph.id, itemID); for (let holeID in layerGraph.holes) removeHole(planData, layerGraph.id, holeID); } } } }); return planData; } function replaceObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog) { let promises = []; switch (modifiedPath[3]) { case "vertices": break; case "holes": let newHoleData = layer.holes.get(modifiedPath[4]); let lineID = newHoleData.line; if (modifiedPath[5] === 'selected') { // I remove only the hole without removing the wall removeHole(planData, layer.id, newHoleData.id); promises.push(addHole(sceneData, planData, layer, newHoleData.id, catalog, actions.holesActions)); } else { layer.lines.get(lineID).holes.forEach(holeID => { removeHole(planData, layer.id, holeID); }); removeLine(planData, layer.id, lineID); promises.push(addLine(sceneData, planData, layer, lineID, catalog, actions.linesActions)); layer.lines.get(lineID).holes.forEach(holeID => { promises.push(addHole(sceneData, planData, layer, holeID, catalog, actions.holesActions)); }); } break; case "lines": removeLine(planData, layer.id, modifiedPath[4]); promises.push(addLine(sceneData, planData, layer, modifiedPath[4], catalog, actions.linesActions)); break; case "areas": removeArea(planData, layer.id, modifiedPath[4]); promises.push(addArea(sceneData, planData, layer, modifiedPath[4], catalog, actions.areaActions)); break; case "items": removeItem(planData, layer.id, modifiedPath[4]); promises.push(addItem(sceneData, planData, layer, modifiedPath[4], catalog, actions.itemsActions)); break; case "visible": if (!layer.visible) { let layerGraph = planData.sceneGraph.layers[layer.id]; for (let lineID in layerGraph.lines) removeLine(planData, layer.id, lineID); for (let areaID in layerGraph.areas) removeArea(planData, layer.id, areaID); for (let itemID in layerGraph.items) removeItem(planData, layer.id, itemID); for (let holeID in layerGraph.holes) removeHole(planData, layer.id, holeID); } else { promises = promises.concat(createLayerObjects(layer, planData, sceneData, actions, catalog)) } break; case "opacity": case "altitude": let layerGraph = planData.sceneGraph.layers[layer.id]; for (let lineID in layerGraph.lines) removeLine(planData, layer.id, lineID); for (let areaID in layerGraph.areas) removeArea(planData, layer.id, areaID); for (let itemID in layerGraph.items) removeItem(planData, layer.id, itemID); for (let holeID in layerGraph.holes) removeHole(planData, layer.id, holeID); promises = promises.concat(createLayerObjects(layer, planData, sceneData, actions, catalog)); } Promise.all(promises).then(values => { updateBoundingBox(planData); }) } function removeObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog) { let promises = []; switch (modifiedPath[3]) { case "lines": // Here I remove the line with all its holes let lineID = modifiedPath[4]; let oldLayer = oldSceneData.layers.get(layer.id); oldLayer.lines.get(lineID).holes.forEach(holeID => { removeHole(planData, layer.id, holeID); }); removeLine(planData, layer.id, lineID); if (modifiedPath.length > 5) { // I removed an hole, so I should add the new line promises.push(addLine(sceneData, planData, layer, lineID, catalog, actions.linesActions)); layer.lines.get(lineID).holes.forEach(holeID => { promises.push(addHole(sceneData, planData, layer, holeID, catalog, actions.holesActions)); }); } break; case "areas": if (modifiedPath.length === 5) { // I am removing an entire area removeArea(planData, layer.id, modifiedPath[4]); } break; case "items": if (modifiedPath.length === 5) { // I am removing an item removeItem(planData, layer.id, modifiedPath[4]); } break; } Promise.all(promises).then(values => { updateBoundingBox(planData); }) } function removeLayer(layerId, planData) { let layerGraph = planData.sceneGraph.layers[layerId]; for (let lineID in layerGraph.lines) removeLine(planData, layerId, lineID); for (let areaID in layerGraph.areas) removeArea(planData, layerId, areaID); for (let itemID in layerGraph.items) removeItem(planData, layerId, itemID); for (let holeID in layerGraph.holes) removeHole(planData, layerId, holeID); delete planData.sceneGraph.layers[layerId]; } function removeHole(planData, layerId, holeToRemoveID) { let holeToRemove = planData.sceneGraph.layers[layerId].holes[holeToRemoveID]; planData.plan.remove(holeToRemove); disposeObject(holeToRemove); delete planData.sceneGraph.layers[layerId].holes[holeToRemoveID]; delete planData.sceneGraph.LODs[holeToRemoveID]; holeToRemove = null; updateBoundingBox(planData); } function removeLine(planData, layerId, lineID) { let line3D = planData.sceneGraph.layers[layerId].lines[lineID]; planData.plan.remove(line3D); disposeObject(line3D); delete planData.sceneGraph.layers[layerId].lines[lineID]; delete planData.sceneGraph.LODs[lineID]; line3D = null; updateBoundingBox(planData); } function removeArea(planData, layerId, areaID) { let area3D = planData.sceneGraph.layers[layerId].areas[areaID]; planData.plan.remove(area3D); disposeObject(area3D); delete planData.sceneGraph.layers[layerId].areas[areaID]; delete planData.sceneGraph.LODs[areaID]; area3D = null; updateBoundingBox(planData); } function removeItem(planData, layerId, itemID) { let item3D = planData.sceneGraph.layers[layerId].items[itemID]; planData.plan.remove(item3D); disposeObject(item3D); delete planData.sceneGraph.layers[layerId].items[itemID]; delete planData.sceneGraph.LODs[itemID]; item3D = null; updateBoundingBox(planData); } function addObject(modifiedPath, layer, planData, actions, sceneData, oldSceneData, catalog) { let promises = []; switch (modifiedPath[3]) { case "lines": if (modifiedPath.length === 5) { // I have to add a line promises.push(addLine(sceneData, planData, layer, modifiedPath[4], catalog, actions.linesActions)); } break; case "areas": if (modifiedPath.length === 5) { // I have to add an area promises.push(addArea(sceneData, planData, layer, modifiedPath[4], catalog, actions.areaActions)); } break; case "items": if (modifiedPath.length === 5) { // I have to add an area promises.push(addItem(sceneData, planData, layer, modifiedPath[4], catalog, actions.itemsActions)); } break; } Promise.all(promises).then(values => { updateBoundingBox(planData); }) } function addHole(sceneData, planData, layer, holeID, catalog, holesActions) { let holeData = layer.holes.get(holeID); // Create the hole object return catalog.getElement(holeData.type).render3D(holeData, layer, sceneData).then(object => { if (object instanceof Three.LOD) { planData.sceneGraph.LODs[holeID] = object } let pivot = new Three.Object3D(); pivot.add(object); let line = layer.lines.get(holeData.line); // First of all I need to find the vertices of this line let vertex0 = layer.vertices.get(line.vertices.get(0)); let vertex1 = layer.vertices.get(line.vertices.get(1)); let offset = holeData.offset; if (vertex0.x > vertex1.x) { let tmp = vertex0; vertex0 = vertex1; vertex1 = tmp; offset = 1 - offset; } let distance = Math.sqrt(Math.pow(vertex0.x - vertex1.x, 2) + Math.pow(vertex0.y - vertex1.y, 2)); let alpha = Math.asin((vertex1.y - vertex0.y) / distance); let boundingBox = new Three.Box3().setFromObject(pivot); let center = [ (boundingBox.max.x - boundingBox.min.x) / 2 + boundingBox.min.x, (boundingBox.max.y - boundingBox.min.y) / 2 + boundingBox.min.y, (boundingBox.max.z - boundingBox.min.z) / 2 + boundingBox.min.z]; let holeAltitude = holeData.properties.get('altitude').get('length'); let holeHeight = holeData.properties.get('height').get('length'); pivot.rotation.y = alpha; pivot.position.x = vertex0.x + distance * offset * Math.cos(alpha) - center[2] * Math.sin(alpha); pivot.position.y = holeAltitude + holeHeight / 2 - center[1] + layer.altitude; pivot.position.z = -vertex0.y - distance * offset * Math.sin(alpha) - center[2] * Math.cos(alpha); planData.plan.add(pivot); planData.sceneGraph.layers[layer.id].holes[holeData.id] = pivot; applyInteract(pivot, () => { return holesActions.selectHole(layer.id, holeData.id) }); let opacity = layer.opacity; if (holeData.selected) { opacity = 1; } applyOpacity(pivot, opacity); }); } function addLine(sceneData, planData, layer, lineID, catalog, linesActions) { let line = layer.lines.get(lineID); // First of all I need to find the vertices of this line let vertex0 = layer.vertices.get(line.vertices.get(0)); let vertex1 = layer.vertices.get(line.vertices.get(1)); if (vertex0.x > vertex1.x) { let tmp = vertex0; vertex0 = vertex1; vertex1 = tmp; } return catalog.getElement(line.type).render3D(line, layer, sceneData).then(line3D => { if (line3D instanceof Three.LOD) { planData.sceneGraph.LODs[line.id] = line3D; } let pivot = new Three.Object3D(); pivot.add(line3D); pivot.position.x = vertex0.x; pivot.position.y = layer.altitude; pivot.position.z = -vertex0.y; planData.plan.add(pivot); planData.sceneGraph.layers[layer.id].lines[lineID] = pivot; applyInteract(pivot, () => { return linesActions.selectLine(layer.id, line.id); }); let opacity = layer.opacity; if (line.selected) { opacity = 1; } applyOpacity(pivot, opacity); }); } function addArea(sceneData, planData, layer, areaID, catalog, areaActions) { let area = layer.areas.get(areaID); let interactFunction = () => { areaActions.selectArea(layer.id, area.id); }; return catalog.getElement(area.type).render3D(area, layer, sceneData).then(area3D => { if (area3D instanceof Three.LOD) { planData.sceneGraph.LODs[areaID] = area3D } let pivot = new Three.Object3D(); pivot.add(area3D); pivot.position.y = layer.altitude; planData.plan.add(pivot); planData.sceneGraph.layers[layer.id].areas[area.id] = pivot; applyInteract(pivot, interactFunction); let opacity = layer.opacity; if (area.selected) { opacity = 1; } applyOpacity(pivot, opacity); }); } function addItem(sceneData, planData, layer, itemID, catalog, itemsActions) { let item = layer.items.get(itemID); return catalog.getElement(item.type).render3D(item, layer, sceneData).then(item3D => { if (item3D instanceof Three.LOD) { planData.sceneGraph.LODs[itemID] = item3D } let pivot = new Three.Object3D(); pivot.add(item3D); pivot.rotation.y = item.rotation * Math.PI / 180; pivot.position.x = item.x; pivot.position.y = layer.altitude; pivot.position.z = -item.y; applyInteract(item3D, () => { itemsActions.selectItem(layer.id, item.id); } ); let opacity = layer.opacity; if (item.selected) { opacity = 1; } applyOpacity(pivot, opacity); planData.plan.add(pivot); planData.sceneGraph.layers[layer.id].items[item.id] = pivot; }); } // Apply interact function to children of an Object3D function applyInteract(object, interactFunction) { object.traverse(function (child) { if (child instanceof Three.Mesh) { child.interact = interactFunction; } }); } // Apply opacity to children of an Object3D function applyOpacity(object, opacity) { object.traverse(function (child) { if (child instanceof Three.Mesh) { if (child.material instanceof Three.MultiMaterial) { child.material.materials.forEach(materialChild => { materialChild.transparent = true; if (materialChild.maxOpacity) { materialChild.opacity = Math.min(materialChild.maxOpacity, opacity); } else if (materialChild.opacity && materialChild.opacity > opacity) { materialChild.maxOpacity = materialChild.opacity; materialChild.opacity = opacity; } }); } else if (child.material instanceof Array) { child.material.forEach(material => { material.transparent = true; if (material.maxOpacity) { material.opacity = Math.min(material.maxOpacity, opacity); } else if (material.opacity && material.opacity > opacity) { material.maxOpacity = material.opacity; material.opacity = opacity; } }); } else { child.material.transparent = true; if (child.material.maxOpacity) { child.material.opacity = Math.min(child.material.maxOpacity, opacity); } else if (child.material.opacity && child.material.opacity > opacity) { child.material.maxOpacity = child.material.opacity; child.material.opacity = opacity; } } } }); } function updateBoundingBox(planData) { let newBoundingBox = new Three.Box3().setFromObject(planData.plan); if (isFinite(newBoundingBox.max.x) && isFinite(newBoundingBox.min.x) && isFinite(newBoundingBox.max.y) && isFinite(newBoundingBox.min.y) && isFinite(newBoundingBox.max.z) && isFinite(newBoundingBox.min.z)) { let newCenter = new Three.Vector3( ( newBoundingBox.max.x - newBoundingBox.min.x ) / 2 + newBoundingBox.min.x, ( newBoundingBox.max.y - newBoundingBox.min.y ) / 2 + newBoundingBox.min.y, ( newBoundingBox.max.z - newBoundingBox.min.z ) / 2 + newBoundingBox.min.z ); planData.plan.position.sub(newCenter); planData.grid.position.sub(newCenter); newBoundingBox.min.sub(newCenter); newBoundingBox.max.sub(newCenter); planData.boundingBox = newBoundingBox; } } /** * Filter the array of diffs * @param diffArray * @param sceneData * @param oldSceneData * @returns {Array} */ function filterDiffs(diffArray, sceneData, oldSceneData) { return minimizeRemoveDiffsWhenSwitchingLayers( minimizeChangePropertiesDiffs(diffArray, sceneData, oldSceneData), sceneData, oldSceneData); } /** * Reduces the number of remove diffs when switching an hidden layer * @param diffArray the array of the diffs * @param sceneData * @param oldSceneData * @returns {Array} */ function minimizeRemoveDiffsWhenSwitchingLayers(diffArray, sceneData, oldSceneData) { let foundDiff; let i; for (i = 0; i < diffArray.length && !foundDiff; i++) { if (diffArray[i].path === "/selectedLayer") { foundDiff = diffArray[i]; } } if (foundDiff) { if (!sceneData.layers.get(oldSceneData.selectedLayer).visible) { return diffArray.filter(diff => { return !(diff.path.endsWith("/selected") && diff.path.startsWith("/layers/" + oldSceneData.selectedLayer)) && !(diff.op === "remove" && diff.path.includes(oldSceneData.selectedLayer)); }) } } return diffArray; } /** * Reduces the number of change properties diffs * @param diffArray the array of the diffs * @param sceneData * @param oldSceneData * @returns {Array} */ function minimizeChangePropertiesDiffs(diffArray, sceneData, oldSceneData) { let idsFound = {}; return diffArray.filter(diff => { let split = diff.path.split('/'); if (split[5] === 'properties') { return idsFound[split[4]] ? false : ( idsFound[split[4]] = 1 ); } else if (split[5] === "misc") { // Remove misc changes return false; } return true; }); }