Home Reference Source

scripts/items/item.js

import {Mesh, Matrix4, Vector2, Vector3, BoxGeometry, BoxHelper, Box3, MeshBasicMaterial, MeshStandardMaterial, AdditiveBlending} from 'three';
import {CanvasTexture, PlaneGeometry, DoubleSide} from 'three';
import {Color} from 'three';
import {Utils} from '../core/utils.js';
import {Dimensioning} from '../core/dimensioning.js';

/**
 * An Item is an abstract entity for all things placed in the scene, e.g. at
 * walls or on the floor.
 */
export class Item extends Mesh
{
	/**
	 * Constructs an item.
	 *
	 * @param model
	 *            TODO
	 * @param metadata
	 *            TODO
	 * @param geometry
	 *            TODO
	 * @param material
	 *            TODO
	 * @param position
	 *            TODO
	 * @param rotation
	 *            TODO
	 * @param scale
	 *            TODO
	 */
	constructor(model, metadata, geometry, material, position, rotation, scale, isgltf=false)
	{
		super();

		this.model = model;
		this.metadata = metadata;

		/** */
		this.errorGlow = new Mesh();
		/** */
		this.hover = false;
		/** */
		this.selected = false;
		/** */
		this.highlighted = false;
		/** */
		this.error = false;
		/** */
		this.emissiveColor = 0x444444;
		/** Does this object affect other floor items */
		this.obstructFloorMoves = true;
		/** */
		this.position_set = false;
		/** Show rotate option in context menu */
		this.allowRotate = true;
		/** */
		this.fixed = false;
		/** dragging */
		this.dragOffset = new Vector3();
		/** */
		this.halfSize = new Vector3(0,0,0);
		this.bhelper = null;

		this.scene = this.model.scene;

		if(!isgltf)
		{
				this.geometry = geometry;
				this.material = material;
				// center in its boundingbox
				this.geometry.computeBoundingBox();
				this.geometry.applyMatrix(new Matrix4().makeTranslation(- 0.5 * (this.geometry.boundingBox.max.x + this.geometry.boundingBox.min.x),- 0.5 * (this.geometry.boundingBox.max.y + this.geometry.boundingBox.min.y),- 0.5 * (this.geometry.boundingBox.max.z + this.geometry.boundingBox.min.z)));
				this.geometry.computeBoundingBox();
		}
		else
		{
				var objectBox = new Box3();
				objectBox.setFromObject(geometry);
				var hsize = objectBox.max.clone().sub(objectBox.min).multiplyScalar(0.5);
				this.geometry = new BoxGeometry(hsize.x*0.5, hsize.y*0.5, hsize.z*0.5);
				this.material =  new MeshStandardMaterial({color: 0x000000, wireframe: true, visible:false});
				this.geometry.computeBoundingBox();
				this.add(geometry);
		}

		if(!this.material.color)
		{
			this.material.color = new Color('#FFFFFF');
		}
		this.wirematerial = new MeshBasicMaterial({color: 0x000000, wireframe: true});

		this.errorColor = 0xff0000;

		this.resizable = metadata.resizable;

		this.castShadow = true;
		this.receiveShadow = false;

		this.originalmaterial = material;
		this.texture = this.material.texture;

		this.position_set = false;
		if (position)
		{
			this.position.copy(position);
			this.position_set = true;
		}

		this.halfSize = this.objectHalfSize();
		this.canvasWH = document.createElement('canvas');
		this.canvasWH.width = this.getWidth()+1.0;
		this.canvasWH.height = this.getHeight()+1.0;

		this.canvascontextWH = this.canvasWH.getContext('2d');
		this.canvasTextureWH = new CanvasTexture(this.canvasWH);
		this.canvasMaterialWH = new MeshBasicMaterial({map:this.canvasTextureWH, side: DoubleSide, transparent:true});
		this.canvasPlaneWH = new Mesh(new PlaneGeometry(this.getWidth(), this.getHeight(), 1, 1), this.canvasMaterialWH);
		this.canvasPlaneWH.scale.set(1, 1, 1);
		this.canvasPlaneWH.position.set(0, 0, this.getDepth()*0.5 + 0.3);

		this.canvasWD = document.createElement('canvas');
		this.canvasWD.width = this.getWidth()+1.0;
		this.canvasWD.height = this.getDepth()+1.0;

		this.canvascontextWD = this.canvasWD.getContext('2d');
		this.canvasTextureWD = new CanvasTexture(this.canvasWD);
		this.canvasMaterialWD = new MeshBasicMaterial({map:this.canvasTextureWD, side: DoubleSide, transparent:true});
		this.canvasPlaneWD = new Mesh(new PlaneGeometry(this.getWidth(), this.getDepth(), 1, 1), this.canvasMaterialWD);
		this.canvasPlaneWD.rotateX(-Math.PI * 0.5);
		this.canvasPlaneWD.scale.set(1, 1, 1);
		this.canvasPlaneWD.position.set(0, this.getHeight()*0.5 + 0.3, 0);
		this.canvasPlaneWH.visible = this.canvasPlaneWD.visible = false;

    this.add(this.canvasPlaneWH);
		this.add(this.canvasPlaneWD);
		this.resizeProportionally = true;

		if (rotation)
		{
			this.rotation.y = rotation;
		}

		if (scale != null)
		{
			this.setScale(scale.x, scale.y, scale.z);
		}

		if(this.metadata.materialColors)
		{
			if(this.metadata.materialColors.length)
			{
				if(this.material.length)
				{
					for (var i=0;i<this.metadata.materialColors.length;i++)
					{
						this.material[i].color = new Color(this.metadata.materialColors[i]);
					}
				}
				else
				{
					this.material.color = new Color(this.metadata.materialColors[0]);
				}
			}
		}
	}


	updateCanvasTexture(canvas, context, material, w, h, wPrefix, hPrefix)
	{
		if(w < 1 || h < 1)
		{
			return;
		}

		wPrefix = (wPrefix) ? wPrefix: 'w:';
		hPrefix = (hPrefix) ? hPrefix: 'h:';

		w *= 3;
		h *= 3;

		canvas.width = w;
		canvas.height = h;
		canvas.style.letterSpacing = '-22.5px';

		context.font = 'bold 45pt Courier';
		context.fillStyle = '#DADADA99';
		context.fillRect(0, 0, w, h);
		context.textAlign = 'center';
		context.textBaseline = 'middle';

		context.lineWidth = 3;
		context.setLineDash([1, 2]);
		context.strokeStyle = '#000000';

		context.beginPath();
		context.moveTo(0,h*0.5);
		context.lineTo(w,h*0.5);
		context.closePath();
		context.stroke();

		context.beginPath();
		context.moveTo(w * 0.125, 0);
		context.lineTo(w * 0.125, h);
		context.closePath();
		context.stroke();

		context.lineWidth = 1;
		context.setLineDash([0]);
		context.strokeStyle = '#0000FF';
		context.strokeText(wPrefix+Dimensioning.cmToMeasure(w/3), w*0.5, h*0.5);

		context.fillStyle = '#FF0000';
		context.fillText(wPrefix+Dimensioning.cmToMeasure(w/3), w*0.5, h*0.5);

		context.translate(w*0.125, 0);
		context.rotate(Math.PI * 0.5);
		context.strokeStyle = '#0000FF';
		context.strokeText(hPrefix+Dimensioning.cmToMeasure(h/3), h*0.5, 0);

		context.fillStyle = '#FF0000';
		context.fillText(hPrefix+Dimensioning.cmToMeasure(h/3), h*0.5, 0);
		context.restore();
		material.map.needsUpdate = true;
	}

	switchWireframe(flag)
	{
		this.material = (flag)? this.wirematerial : this.originalmaterial;
	}

	/** */
	remove()
	{
		this.scene.removeItem(this);
	}

	/** */
	resize(height, width, depth)
	{
		var x = width / this.getWidth();
		var y = height / this.getHeight();
		var z = depth / this.getDepth();

		if(this.resizeProportionally)
		{
			if(Math.abs(width - this.getWidth()) > 0.1)
			{
				this.setScale(x, x, x);
			}
			else if(Math.abs(height - this.getHeight()) > 0.1)
			{
				this.setScale(y, y, y);
			}
			else
			{
				this.setScale(z, z, z);
			}
			return;
		}
		this.setScale(x, y, z);
	}

	getMaterial()
	{
		return this.material;
	}

	getMaterialColor(index)
	{
		index = (index)? index : 0;
		if(this.material.length)
		{
			return '#'+this.material[index].color.getHexString();
		}
		return '#'+this.material.color.getHexString();
	}

	// Always send an hexadecimal string value for color - ex. '#FFFFFF'
	setMaterialColor(color, index)
	{
		var c = new Color(color);
		if(this.material.length)
		{
			index = (index) ? index : 0;
			this.material[index].color = c;
			return;
		}
		this.material.color = c;
	}

	/** */
	setScale(x, y, z)
	{
		var scaleVec = new Vector3(x, y, z);
		this.halfSize.multiply(scaleVec);
		scaleVec.multiply(this.scale);
		this.scale.set(scaleVec.x, scaleVec.y, scaleVec.z);
		this.resized();
		if(this.bhelper)
		{
			this.bhelper.update();
		}

//		this.updateCanvasTexture(canvas, context, material, w, h);
		this.updateCanvasTexture(this.canvasWH, this.canvascontextWH, this.canvasMaterialWH, this.getWidth(), this.getHeight(), 'w:', 'h:');
		this.updateCanvasTexture(this.canvasWD, this.canvascontextWD, this.canvasMaterialWD, this.getWidth(), this.getDepth(), 'w:', 'd:');

		this.scene.needsUpdate = true;

	}

	getProportionalResize()
	{
		return this.resizeProportionally;
	}

	setProportionalResize(flag)
	{
		this.resizeProportionally = flag;
	}

	/** */
	setFixed(fixed)
	{
		this.fixed = fixed;
	}

	/** Subclass can define to take action after a resize. */
	resized()
	{

	}

	/** */
	getHeight()
	{
		return this.halfSize.y * 2.0;
	}

	/** */
	getWidth()
	{
		return this.halfSize.x * 2.0;
	}

	/** */
	getDepth()
	{
		return this.halfSize.z * 2.0;
	}

	/** */
	placeInRoom()
	{

	}

	/** */
	initObject()
	{
		this.placeInRoom();
		// An ugly hack to increase the size of gltf models
		if(this.halfSize.x < 1.0)
		{
			this.resize(this.getHeight()*300, this.getWidth()*300, this.getDepth()*300);
		}
		this.bhelper = new BoxHelper(this);
		this.scene.add(this.bhelper);
		this.bhelper.visible = false;
		// select and stuff
		this.scene.needsUpdate = true;

	}

	/** */
	removed()
	{
	}

	/** on is a bool */
	updateHighlight()
	{
		var on = this.hover || this.selected;
		this.highlighted = on;
		var hex = on ? this.emissiveColor : 0x000000;
		if(this.material)
		{
			if(this.material.length)
			{
				this.material.forEach((material) => {
					// TODO_Ekki emissive doesn't exist anymore?
					material.emissive.setHex(hex);
					this.material.emissive = new Color(hex);
				});
			}
			else
			{
				this.material.emissive.setHex(hex);
				this.material.emissive = new Color(hex);
			}
		}

	}

	/** */
	mouseOver()
	{
		this.hover = true;
		this.updateHighlight();
	}

	/** */
	mouseOff()
	{
		this.hover = false;
		this.updateHighlight();
	}

	/** */
	setSelected()
	{
		this.setScale(1, 1, 1);
		this.selected = true;
		this.bhelper.visible = true;
		this.canvasPlaneWH.visible = this.canvasPlaneWD.visible = true;
		this.updateHighlight();
	}

	/** */
	setUnselected()
	{
		this.selected = false;
		this.bhelper.visible = false;
		this.canvasPlaneWH.visible = this.canvasPlaneWD.visible = false;
		this.updateHighlight();
	}

	/** intersection has attributes point (vec3) and object (THREE.Mesh) */
	clickPressed(intersection)
	{
		this.dragOffset.copy(intersection.point).sub(this.position);
	}

	/** */
	clickDragged(intersection)
	{
		if (intersection)
		{
			this.moveToPosition(intersection.point.sub(this.dragOffset), intersection);
		}
	}

	/** */
	rotate(intersection)
	{
		if (intersection)
		{
			var angle = Utils.angle(new Vector2(0, 1), new Vector2(intersection.point.x - this.position.x,intersection.point.z - this.position.z));
			var snapTolerance = Math.PI / 16.0;
			// snap to intervals near Math.PI/2
			for (var i = -4; i <= 4; i++)
			{
				if (Math.abs(angle - (i * (Math.PI / 2))) < snapTolerance)
				{
					angle = i * (Math.PI / 2);
					break;
				}
			}
			this.rotation.y = angle;
		}
	}

	/** */
	moveToPosition(vec3)
	{
		this.position.copy(vec3);
		if(this.bhelper)
		{
			this.bhelper.update();
		}
	}

	/** */
	clickReleased()
	{
		if (this.error)
		{
			this.hideError();
		}
	}

	/**
	 * Returns an array of planes to use other than the ground plane for passing
	 * intersection to clickPressed and clickDragged
	 */
	customIntersectionPlanes()
	{
		return [];
	}

	/**
	 * returns the 2d corners of the bounding polygon
	 *
	 * offset is Vector3 (used for getting corners of object at a new position)
	 *
	 * TODO: handle rotated objects better!
	 */
	getCorners(xDim, yDim, position)
	{
		position = position || this.position;
		var halfSize = this.halfSize.clone();
		var c1 = new Vector3(-halfSize.x, 0, -halfSize.z);
		var c2 = new Vector3(halfSize.x, 0, -halfSize.z);
		var c3 = new Vector3(halfSize.x, 0, halfSize.z);
		var c4 = new Vector3(-halfSize.x, 0, halfSize.z);

		var transform = new Matrix4();
		// console.log(this.rotation.y);
		transform.makeRotationY(this.rotation.y); // + Math.PI/2)

		c1.applyMatrix4(transform);
		c2.applyMatrix4(transform);
		c3.applyMatrix4(transform);
		c4.applyMatrix4(transform);

		c1.add(position);
		c2.add(position);
		c3.add(position);
		c4.add(position);

		// halfSize.applyMatrix4(transform);

		// var min = position.clone().sub(halfSize);
		// var max = position.clone().add(halfSize);

		var corners = [{ x: c1.x, y: c1.z },{ x: c2.x, y: c2.z },{ x: c3.x, y: c3.z },{ x: c4.x, y: c4.z }];
		return corners;
	}

	/** */
	isValidPosition()
	{
		return false;
	}

	/** */
	showError(vec3)
	{
		vec3 = vec3 || this.position;
		if (!this.error)
		{
			this.error = true;
			this.errorGlow = this.createGlow(this.errorColor, 0.8, true);
			this.scene.add(this.errorGlow);
		}
		this.errorGlow.position.copy(vec3);
	}

	/** */
	hideError()
	{
		if (this.error)
		{
			this.error = false;
			this.scene.remove(this.errorGlow);
		}
	}

	/** */
	objectHalfSize()
	{
		// var objectBox = new Box3();
		// objectBox.setFromObject(this);
    this.geometry.computeBoundingBox();
    var objectBox = this.geometry.boundingBox.clone();
		return objectBox.max.clone().sub(objectBox.min).divideScalar(2);
	}

	/** */
	createGlow(color, opacity, ignoreDepth)
	{
		ignoreDepth = ignoreDepth || false;
		opacity = opacity || 0.2;
		var glowMaterial = new MeshBasicMaterial({color: color, blending: AdditiveBlending, opacity: 0.2, transparent: true, depthTest: !ignoreDepth});
		var glow = new Mesh(this.geometry.clone(), glowMaterial);
		glow.position.copy(this.position);
		glow.rotation.copy(this.rotation);
		glow.scale.copy(this.scale);
		return glow;
	}


	getMetaData()
	{
		var matattribs = [];
		if(this.material.length)
		{
			this.material.forEach((mat)=>{
				matattribs.push('#'+mat.color.getHexString());
			});

		}
		else
		{
			matattribs.push('#'+this.material.color.getHexString());
		}
		return {item_name: this.metadata.itemName,
			item_type: this.metadata.itemType, format: this.metadata.format, model_url: this.metadata.modelUrl,
			xpos: this.position.x, ypos: this.position.y, zpos: this.position.z,
			rotation: this.rotation.y,
			scale_x: this.scale.x, scale_y: this.scale.y,scale_z: this.scale.z,fixed: this.fixed,
			material_colors: matattribs};
	}
}