@shopware-ag/dive
Version:
Shopware Spatial Framework
186 lines (152 loc) • 6.24 kB
text/typescript
import { Mesh, MeshStandardMaterial, Raycaster, Vector3 } from 'three';
import { PRODUCT_LAYER_MASK } from '../constant/VisibilityLayerMask';
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import { findSceneRecursive } from '../helper/findSceneRecursive/findSceneRecursive';
import { type COMMaterial } from '../com/types';
import { DIVENode } from '../node/Node';
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 DIVEModel extends DIVENode {
readonly isDIVEModel: true = true;
private _mesh: Mesh | null = null;
private _material: MeshStandardMaterial | null = null;
public SetModel(gltf: GLTF): void {
this.clear();
this._boundingBox.makeEmpty();
gltf.scene.traverse((child) => {
child.castShadow = true;
child.receiveShadow = true;
child.layers.mask = this.layers.mask;
this._boundingBox.expandByObject(child);
// only search for first mesh for now
if (!this._mesh && 'isMesh' in child) {
this._mesh = child as Mesh;
// if the material is already set, use it, otherwise set it from the model's material
if (this._material) {
this._mesh.material = this._material;
} else {
this._material = (child as Mesh)
.material as MeshStandardMaterial;
}
}
});
this.add(gltf.scene);
}
public SetMaterial(material: Partial<COMMaterial>): void {
// if there is no material, create a new one
if (!this._material) {
this._material = new MeshStandardMaterial();
}
if (material.vertexColors !== undefined) {
this._material.vertexColors = material.vertexColors;
}
// apply color if supplied
if (material.color !== undefined) {
this._material.color.set(material.color);
}
// apply albedo map if supplied
if (material.map !== undefined) {
this._material.map = material.map;
}
// apply normal map
if (material.normalMap !== undefined) {
this._material.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) {
this._material.roughness = material.roughness;
}
if (material.roughnessMap !== undefined) {
this._material.roughnessMap = material.roughnessMap;
if (this._material.roughnessMap) {
this._material.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) {
this._material.metalness = material.metalness;
}
if (material.metalnessMap !== undefined) {
this._material.metalnessMap = material.metalnessMap;
if (this._material.metalnessMap) {
this._material.metalness = 1.0;
}
}
// if the mesh is already set, update the material
if (this._mesh) {
this._mesh.material = this._material;
}
}
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(
'DIVEModel: 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();
}
}
}