UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

182 lines (160 loc) 5.83 kB
/* @license * Copyright 2020 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {BufferGeometry, DoubleSide, Float32BufferAttribute, Material, Mesh, MeshBasicMaterial, PlaneBufferGeometry, Vector2, Vector3} from 'three'; import {Damper} from './Damper.js'; import {ModelScene} from './ModelScene.js'; import {Side} from './Shadow.js'; const RADIUS = 0.2; const LINE_WIDTH = 0.03; const MAX_OPACITY = 0.75; const SEGMENTS = 12; const DELTA_PHI = Math.PI / (2 * SEGMENTS); const vector2 = new Vector2(); /** * Adds a quarter-annulus of vertices to the array, centered on cornerX, * cornerY. */ const addCorner = (vertices: Array<number>, cornerX: number, cornerY: number) => { let phi = cornerX > 0 ? (cornerY > 0 ? 0 : -Math.PI / 2) : (cornerY > 0 ? Math.PI / 2 : Math.PI); for (let i = 0; i <= SEGMENTS; ++i) { vertices.push( cornerX + (RADIUS - LINE_WIDTH) * Math.cos(phi), cornerY + (RADIUS - LINE_WIDTH) * Math.sin(phi), 0, cornerX + RADIUS * Math.cos(phi), cornerY + RADIUS * Math.sin(phi), 0); phi += DELTA_PHI; } }; /** * This class is a set of two coincident planes. The first is just a cute box * outline with rounded corners and damped opacity to indicate the floor extents * of a scene. It is purposely larger than the scene's bounding box by RADIUS on * all sides so that small scenes are still visible / selectable. Its center is * actually carved out by vertices to ensure its fragment shader doesn't add * much time. * * The child plane is a simple plane with the same extents for use in hit * testing (translation is triggered when the touch hits the plane, rotation * otherwise). */ export class PlacementBox extends Mesh { private hitPlane: Mesh; private shadowHeight: number; private side: Side; private goalOpacity: number; private opacityDamper: Damper; constructor(scene: ModelScene, side: Side) { const geometry = new BufferGeometry(); const triangles: Array<number> = []; const vertices: Array<number> = []; const {size, boundingBox} = scene; const x = size.x / 2; const y = (side === 'back' ? size.y : size.z) / 2; addCorner(vertices, x, y); addCorner(vertices, -x, y); addCorner(vertices, -x, -y); addCorner(vertices, x, -y); const numVertices = vertices.length / 3; for (let i = 0; i < numVertices - 2; i += 2) { triangles.push(i, i + 1, i + 3, i, i + 3, i + 2); } const i = numVertices - 2; triangles.push(i, i + 1, 1, i, 1, 0); geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3)); geometry.setIndex(triangles); super(geometry); this.side = side; const material = this.material as MeshBasicMaterial; material.side = DoubleSide; material.transparent = true; material.opacity = 0; this.goalOpacity = 0; this.opacityDamper = new Damper(); this.hitPlane = new Mesh(new PlaneBufferGeometry(2 * (x + RADIUS), 2 * (y + RADIUS))); this.hitPlane.visible = false; this.add(this.hitPlane); boundingBox.getCenter(this.position); switch (side) { case 'bottom': this.rotateX(-Math.PI / 2); this.shadowHeight = boundingBox.min.y; this.position.y = this.shadowHeight; break; case 'back': this.shadowHeight = boundingBox.min.z; this.position.z = this.shadowHeight; } scene.target.add(this); } /** * Get the world hit position if the touch coordinates hit the box, and null * otherwise. Pass the scene in to get access to its raycaster. */ getHit(scene: ModelScene, screenX: number, screenY: number): Vector3|null { vector2.set(screenX, -screenY); this.hitPlane.visible = true; const hitResult = scene.positionAndNormalFromPoint(vector2, this.hitPlane); this.hitPlane.visible = false; return hitResult == null ? null : hitResult.position; } /** * Offset the height of the box relative to the bottom of the scene. Positive * is up, so generally only negative values are used. */ set offsetHeight(offset: number) { if (this.side === 'back') { this.position.z = this.shadowHeight + offset; } else { this.position.y = this.shadowHeight + offset; } } get offsetHeight(): number { if (this.side === 'back') { return this.position.z - this.shadowHeight; } else { return this.position.y - this.shadowHeight; } } /** * Set the box's visibility; it will fade in and out. */ set show(visible: boolean) { this.goalOpacity = visible ? MAX_OPACITY : 0; } /** * Call on each frame with the frame delta to fade the box. */ updateOpacity(delta: number) { const material = this.material as MeshBasicMaterial; material.opacity = this.opacityDamper.update(material.opacity, this.goalOpacity, delta, 1); this.visible = material.opacity > 0; } /** * Call this to clean up Three's cache when you remove the box. */ dispose() { const {geometry, material} = this.hitPlane; geometry.dispose(); (material as Material).dispose(); this.geometry.dispose(); (this.material as Material).dispose(); } }