UNPKG

@shopware-ag/dive

Version:

Shopware Spatial Framework

300 lines (257 loc) 9.86 kB
import { BoxGeometry, BufferAttribute, BufferGeometry, Color, ConeGeometry, CylinderGeometry, Mesh, MeshStandardMaterial, Raycaster, SphereGeometry, Vector3, } from 'three'; import { PRODUCT_LAYER_MASK } from '../constant/VisibilityLayerMask'; import { findSceneRecursive } from '../helper/findSceneRecursive/findSceneRecursive'; import { DIVENode } from '../node/Node'; import { type COMGeometry, type COMMaterial } from '../com/types'; import { DIVECommunication } from '../com/Communication'; /** * A basic model class. * * It does calculate it's own bounding box which is used for positioning on the floor. * * Can be moved and selected. * * @module */ export class DIVEPrimitive extends DIVENode { readonly isDIVEPrimitive: true = true; private _mesh: Mesh; constructor() { super(); this._mesh = new Mesh(); this._mesh.layers.mask = PRODUCT_LAYER_MASK; this._mesh.castShadow = true; this._mesh.receiveShadow = true; this._mesh.material = new MeshStandardMaterial(); this.add(this._mesh); } public SetGeometry(geometry: COMGeometry): void { const geo = this.assembleGeometry(geometry); if (!geo) return; this._mesh.geometry = geo; this._boundingBox.setFromObject(this._mesh); } public SetMaterial(material: Partial<COMMaterial>): void { const primitiveMaterial = this._mesh.material as MeshStandardMaterial; if (material.vertexColors !== undefined) { primitiveMaterial.vertexColors = material.vertexColors; } // apply color if supplied if (material.color !== undefined) { primitiveMaterial.color = new Color(material.color); } // apply albedo map if supplied if (material.map !== undefined) { primitiveMaterial.map = material.map; } // apply normal map if (material.normalMap !== undefined) { primitiveMaterial.normalMap = material.normalMap; } // set roughness value // if supplied, apply roughness map // if we applied a roughness map, set roughness to 1.0 if (material.roughness !== undefined) { primitiveMaterial.roughness = material.roughness; } if (material.roughnessMap !== undefined) { primitiveMaterial.roughnessMap = material.roughnessMap; if (primitiveMaterial.roughnessMap) { primitiveMaterial.roughness = 1.0; } } // set metalness value // if supplied, apply metalness map // if we applied a metalness map, set metalness to 1.0 if (material.metalness !== undefined) { primitiveMaterial.metalness = material.metalness; } if (material.metalnessMap !== undefined) { primitiveMaterial.metalnessMap = material.metalnessMap; if (primitiveMaterial.metalnessMap) { primitiveMaterial.metalness = 0.0; } } // if the mesh is already set, update the material if (this._mesh) this._mesh.material = primitiveMaterial; } public PlaceOnFloor(): void { // calculate and temporary save world position const worldPos = this.getWorldPosition(this._positionWorldBuffer); const oldWorldPos = worldPos.clone(); // compute the bounding box this._mesh?.geometry?.computeBoundingBox(); const meshBB = this._mesh?.geometry?.boundingBox; // subtract the bounding box min y axis value from the world position y value if (!meshBB || !this._mesh) return; worldPos.y = worldPos.y - this._mesh.localToWorld(meshBB.min.clone()).y; // skip any action when the position did not change if (worldPos.y === oldWorldPos.y) return; DIVECommunication.get(this.userData.id)?.PerformAction( 'UPDATE_OBJECT', { id: this.userData.id, position: worldPos, rotation: this.rotation, scale: this.scale, }, ); } public DropIt(): void { if (!this.parent) { console.warn( 'DIVEPrimitive: DropIt() called on a model that is not in the scene.', this, ); return; } // calculate the bottom center of the bounding box const bottomY = this._boundingBox.min.y * this.scale.y; const bbBottomCenter = this.localToWorld( this._boundingBox.getCenter(new Vector3()).multiply(this.scale), ); bbBottomCenter.y = bottomY + this.position.y; // set up raycaster and raycast all scene objects (product layer) const raycaster = new Raycaster(bbBottomCenter, new Vector3(0, -1, 0)); raycaster.layers.mask = PRODUCT_LAYER_MASK; const intersections = raycaster.intersectObjects( findSceneRecursive(this).Root.children, true, ); // if we hit something, move the model to the top on the hit object's bounding box if (intersections.length > 0) { const mesh = intersections[0].object as Mesh; mesh.geometry.computeBoundingBox(); const meshBB = mesh.geometry.boundingBox!; const worldPos = mesh.localToWorld(meshBB.max.clone()); const oldPos = this.position.clone(); const newPos = this.position .clone() .setY(worldPos.y) .sub(new Vector3(0, bottomY, 0)); this.position.copy(newPos); // if the position changed, update the object in communication if (this.position.y === oldPos.y) return; this.onMove(); } } private assembleGeometry(geometry: COMGeometry): BufferGeometry | null { // reset material to smooth shading (this._mesh.material as MeshStandardMaterial).flatShading = false; switch (geometry.name.toLowerCase()) { case 'cylinder': return this.createCylinderGeometry(geometry); case 'sphere': return this.createSphereGeometry(geometry); case 'pyramid': // set material to flat shading for pyramid (this._mesh.material as MeshStandardMaterial).flatShading = true; return this.createPyramidGeometry(geometry); case 'cube': case 'box': return this.createBoxGeometry(geometry); case 'cone': return this.createConeGeometry(geometry); case 'wall': return this.createWallGeometry(geometry); case 'plane': return this.createPlaneGeometry(geometry); default: { console.warn( 'DIVEPrimitive.assembleGeometry: Invalid geometry type:', geometry.name.toLowerCase(), ); return null; } } } private createCylinderGeometry(geometry: COMGeometry): BufferGeometry { const geo = new CylinderGeometry( geometry.width / 2, geometry.width / 2, geometry.height, 64, ); geo.translate(0, geometry.height / 2, 0); return geo; } private createSphereGeometry(geometry: COMGeometry): BufferGeometry { const geo = new SphereGeometry(geometry.width / 2, 256, 256); return geo; } private createPyramidGeometry(geometry: COMGeometry): BufferGeometry { // prettier-multiline-arrays-next-line-pattern: 3 const vertices = new Float32Array([ -geometry.width / 2, 0, -geometry.depth / 2, // 0 geometry.width / 2, 0, -geometry.depth / 2, // 1 geometry.width / 2, 0, geometry.depth / 2, // 2 -geometry.width / 2, 0, geometry.depth / 2, // 3 0, geometry.height, 0, ]); // prettier-multiline-arrays-next-line-pattern: 3 const indices = new Uint16Array([ 0, 1, 2, 0, 2, 3, 0, 4, 1, 1, 4, 2, 2, 4, 3, 3, 4, 0, ]); const geometryBuffer = new BufferGeometry(); geometryBuffer.setAttribute( 'position', new BufferAttribute(vertices, 3), ); geometryBuffer.setIndex(new BufferAttribute(indices, 1)); geometryBuffer.computeVertexNormals(); geometryBuffer.computeBoundingBox(); geometryBuffer.computeBoundingSphere(); return geometryBuffer; } private createBoxGeometry(geometry: COMGeometry): BufferGeometry { const geo = new BoxGeometry( geometry.width, geometry.height, geometry.depth, ); geo.translate(0, geometry.height / 2, 0); return geo; } private createConeGeometry(geometry: COMGeometry): BufferGeometry { const geo = new ConeGeometry(geometry.width / 2, geometry.height, 256); geo.translate(0, geometry.height / 2, 0); return geo; } private createWallGeometry(geometry: COMGeometry): BufferGeometry { const geo = new BoxGeometry( geometry.width, geometry.height, geometry.depth || 0.05, 16, ); geo.translate(0, geometry.height / 2, 0); return geo; } private createPlaneGeometry(geometry: COMGeometry): BufferGeometry { const geo = new BoxGeometry( geometry.width, geometry.height, geometry.depth, ); geo.translate(0, geometry.height / 2, 0); return geo; } }