@shopware-ag/dive
Version:
Shopware Spatial Framework
300 lines (257 loc) • 9.86 kB
text/typescript
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;
}
}