UNPKG

@jake-niemiec/react-planner

Version:

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

373 lines (295 loc) 12.5 kB
import { Shape, MeshPhongMaterial, ShapeGeometry, TextureLoader, BackSide, FrontSide, DoubleSide, Object3D, Mesh, MeshLambertMaterial, RepeatWrapping, Vector2, BoxHelper, VertexColors } from 'three'; const bboxColor = 0x99c3fb; const halfPI = Math.PI / 2; /** * This function build the closures around the holes and the walls * @param wall: Wall Object * @param color: Box Color * @returns {BoxHelper}: BoxHelper */ const createBoxHelper = (wall, color) => { let box = new BoxHelper(wall, color); box.material.depthTest = false; box.material.linewidth = 2; box.material.vertexColor = VertexColors; box.renderOrder = 1000; return box; }; /** * This function build the closures around the holes and the walls * @param vertex0: Start vertex * @param vertex1: End vertex * @param height: Height of the shape * @param thickness: Thickness of the closure * @returns {{topShape: *[], leftShape: *[]}}: The left and top shape (the others can be computed from these two) */ const buildShapeClosures = (vertex0, vertex1, height, thickness) => { let xDiff = vertex0.x - vertex1.x; let yDiff = vertex0.y - vertex1.y; let distance = Math.sqrt((xDiff * xDiff) + (yDiff * yDiff)); let topShape = [ [vertex0.x, vertex0.y], [vertex0.x + distance, vertex0.y], [vertex0.x + distance, vertex0.y + thickness], [vertex0.x, vertex0.y + thickness]]; let leftShape = [ [vertex0.x, vertex0.y], [vertex0.x + thickness, vertex0.y], [vertex0.x + thickness, vertex0.y + height], [vertex0.x, vertex0.y + height], ]; return { topShape, leftShape }; }; /** * This function will create a shape given a list of coordinates * @param shapeCoords * @returns {Shape} */ const createShape = (shapeCoords) => { let shape = new Shape(); shape.moveTo(shapeCoords[0][0], shapeCoords[0][1]); for (let i = 1; i < shapeCoords.length; i++) { shape.lineTo(shapeCoords[i][0], shapeCoords[i][1]); } return shape; }; /** * Apply a texture to a wall face * @param material: The material of the face * @param texture: The texture to load * @param length: The lenght of the face * @param height: The height of the face */ const applyTexture = (material, texture, length, height) => { let loader = new TextureLoader(); if (texture) { material.map = loader.load(texture.uri); material.needsUpdate = true; material.map.wrapS = RepeatWrapping; material.map.wrapT = RepeatWrapping; material.map.repeat.set(length * texture.lengthRepeatScale, height * texture.heightRepeatScale); if (texture.normal) { material.normalMap = loader.load(texture.normal.uri); material.normalScale = new Vector2(texture.normal.normalScaleX, texture.normal.normalScaleY); material.normalMap.wrapS = RepeatWrapping; material.normalMap.wrapT = RepeatWrapping; material.normalMap.repeat.set(length * texture.normal.lengthRepeatScale, height * texture.normal.heightRepeatScale); } } }; /** * Function that assign UV coordinates to a geometry * @param geometry */ const assignUVs = (geometry) => { geometry.computeBoundingBox(); let { min, max } = geometry.boundingBox; let offset = new Vector2(0 - min.x, 0 - min.y); let range = new Vector2(max.x - min.x, max.y - min.y); geometry.faceVertexUvs[0] = geometry.faces.map((face) => { let v1 = geometry.vertices[face.a]; let v2 = geometry.vertices[face.b]; let v3 = geometry.vertices[face.c]; return [ new Vector2((v1.x + offset.x) / range.x, (v1.y + offset.y) / range.y), new Vector2((v2.x + offset.x) / range.x, (v2.y + offset.y) / range.y), new Vector2((v3.x + offset.x) / range.x, (v3.y + offset.y) / range.y) ]; }); geometry.uvsNeedUpdate = true; }; export default function buildWall(element, layer, scene, textures) { // Get the two vertices of the wall let vertex0 = layer.vertices.get(element.vertices.get(0)); let vertex1 = layer.vertices.get(element.vertices.get(1)); let inverted = false; // The first vertex is the smaller one if (vertex0.x > vertex1.x) { let app = vertex0; vertex0 = vertex1; vertex1 = app; inverted = true; } // Get height and thickness of the wall converting them into the current scene units let height = element.properties.getIn(['height', 'length']); let thickness = element.properties.getIn(['thickness', 'length']); let halfThickness = thickness / 2; let bevelRadius = thickness; // This is useful for linking two walls together let halfBevelRadius = bevelRadius / 2; /* * First of all I need to build the wall shape. We can build it drawing a rectangle * with the left bottom vertex in the origin and where the length is given by the distance between the * two vertices and the height is the height of the wall */ // Compute the distance between the two vertices let xDiff = vertex0.x - vertex1.x; let yDiff = vertex0.y - vertex1.y; let distance = Math.sqrt((xDiff * xDiff) + (yDiff * yDiff)); // I use this workaround to link two adjacent walls together distance += bevelRadius; //TODO: REMOVE WORKAROUND BEVELING // Now I can build the coordinates of the wall shape: /** * * (bevelRadius/2,height) (distance,height) * x------------------x * | | ^ y * | | | * | | | * | | --|-----> x * | | * | | * x------------------x * (bevelRadius/2,0) (distance,0) * */ let wallCoords = [ [0, 0], [distance, 0], [distance, height], [0, height] ]; // Now I compute the rotation angle of the wall (see the bevel radius trick) let alpha = Math.asin((vertex1.y - vertex0.y) / (distance - bevelRadius)); //TODO: REMOVE WORKAROUND BEVELING // We will rotate on the bottom-left vertex, so we need a pivot in the origin let pivot = new Object3D(); // Create a Shape from the coordinates let rectShape = createShape(wallCoords); // Now we have to create the holes for the wall element.holes.forEach(holeID => { let holeData = layer.holes.get(holeID); // Get the sizes of the holes converting them to the scene units let holeWidth = holeData.properties.getIn(['width', 'length']); let holeHeight = holeData.properties.getIn(['height', 'length']); let holeAltitude = holeData.properties.getIn(['altitude', 'length']); let offset = inverted ? 1 - holeData.offset : holeData.offset; // Now I can build the coordinates of the hole shape: // The starting point is at (distance - bevelRadius) * offset + halfBevelRadius - width / 2 and is called startAt /** * * (startAt,holeAltitude + holeHeight) (holeWidth + startAt,holeAltitude + holeHeight) * x-------------------------------------------x * | | * | | * | | * | | * | | * x-------------------------------------------x * (startAt,holeAltitude) (holeWidth + startAt,holeAltitude) * */ let startAt = (distance - bevelRadius) * offset - holeWidth / 2 + halfBevelRadius; // I add 0.00001 to the holeAltitude to avoid a warning with the Three triangulation algorithm let holeCoords = [ [startAt, holeAltitude + 0.00001], [holeWidth + startAt, holeAltitude + 0.00001], [holeWidth + startAt, holeHeight + holeAltitude], [startAt, holeHeight + holeAltitude] ]; // Now I can create the Threeshape of the hole and, push it into the wall shape holes let holeShape = createShape(holeCoords); rectShape.holes.push(holeShape); // At this point I can create a set of rectangles which surrounds the hole let holeClosures = buildShapeClosures( { x: 0, y: 0 }, { x: holeWidth, y: 0 }, holeHeight, thickness ); let topHoleShape = createShape(holeClosures.topShape); let leftHoleShape = createShape(holeClosures.leftShape); let topHoleClosureGeometry = new ShapeGeometry(topHoleShape); let leftHoleClosureGeometry = new ShapeGeometry(leftHoleShape); let meshMeterial = new MeshLambertMaterial({ side: DoubleSide }); let topHoleClosure = new Mesh(topHoleClosureGeometry, meshMeterial); let leftHoleClosure = new Mesh(leftHoleClosureGeometry, meshMeterial); let rightHoleClosure = new Mesh(leftHoleClosureGeometry, meshMeterial); topHoleClosure.rotation.x += halfPI; topHoleClosure.position.set(startAt - halfBevelRadius, holeHeight + holeAltitude, -halfThickness); leftHoleClosure.rotation.y -= halfPI; leftHoleClosure.position.set(startAt - halfBevelRadius, holeAltitude, -halfThickness); rightHoleClosure.rotation.y -= halfPI; rightHoleClosure.position.set(startAt + holeWidth - halfBevelRadius, holeAltitude, -halfThickness); pivot.add(topHoleClosure, leftHoleClosure, rightHoleClosure); if (holeAltitude !== 0) { let bottomHoleClosure = new Mesh(topHoleClosureGeometry, meshMeterial); bottomHoleClosure.rotation.x += halfPI; bottomHoleClosure.position.set(startAt - halfBevelRadius, holeAltitude, -halfThickness); pivot.add(bottomHoleClosure); } }); // Now I can create the geometry of a single face of the wall let wallGeometry = new ShapeGeometry(rectShape); wallGeometry.computeVertexNormals(); // I define two materials (one for every face of the wall) let wallMaterial1 = new MeshPhongMaterial({ side: BackSide }); let wallMaterial2 = new MeshPhongMaterial({ side: FrontSide }); let alphaMaj = alpha > 0; applyTexture(wallMaterial1, textures[element.properties.get('texture' + (alphaMaj ? 'A' : 'B'))], distance, height); applyTexture(wallMaterial2, textures[element.properties.get('texture' + (alphaMaj ? 'B' : 'A'))], distance, height); // Assign the correct UV coordinates assignUVs(wallGeometry); let wall1 = new Mesh(wallGeometry, wallMaterial1); let wall2 = new Mesh(wallGeometry, wallMaterial2); // Expand the wall at the center wall1.position.z -= halfThickness; wall2.position.z += halfThickness; // Change walls x position to link two adjacent walls wall1.position.x -= halfBevelRadius; //TODO: REMOVE WORKAROUND BEVELING wall2.position.x -= halfBevelRadius; //TODO: REMOVE WORKAROUND BEVELING // Rotate the wall around the bottom-left vertex pivot.rotation.y = alpha; // Add the two wall faces to the pivot pivot.add(wall1, wall2); // Build closures for wall let closures = buildShapeClosures({ x: 0, y: 0 }, { x: distance, y: 0 }, height, thickness); let topClosureGeometry = new ShapeGeometry(createShape(closures.topShape)); let leftClosureGeometry = new ShapeGeometry(createShape(closures.leftShape)); let topClosure = new Mesh(topClosureGeometry, new MeshLambertMaterial({ side: BackSide })); let leftClosure = new Mesh(leftClosureGeometry, new MeshLambertMaterial({ side: FrontSide })); let rightClosure = new Mesh(leftClosureGeometry, new MeshLambertMaterial({ side: BackSide })); // Move the wall closures topClosure.rotation.x += halfPI; topClosure.position.z = -halfThickness; topClosure.position.y += height; topClosure.position.x = -halfBevelRadius; //TODO: REMOVE WORKAROUND BEVELING leftClosure.rotation.y -= halfPI; leftClosure.position.z = -halfBevelRadius; leftClosure.position.x = -halfBevelRadius - 1; //TODO: REMOVE WORKAROUND BEVELING rightClosure.rotation.y -= halfPI; rightClosure.position.z = -halfThickness; rightClosure.position.x += distance - 1; rightClosure.position.x = -halfBevelRadius; //TODO: REMOVE WORKAROUND BEVELING pivot.add(topClosure, leftClosure, rightClosure); // If the wall is selected show a bounding box around it if (element.selected) { pivot.add( createBoxHelper(wall1, bboxColor), createBoxHelper(wall2, bboxColor), createBoxHelper(topClosure, bboxColor), createBoxHelper(leftClosure, bboxColor), createBoxHelper(rightClosure, bboxColor) ); } return Promise.resolve(pivot); }