@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
203 lines (172 loc) • 5.8 kB
text/typescript
/* @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 {Matrix3, Mesh, Quaternion, Triangle, Vector3} from 'three';
import {CSS2DObject} from 'three/examples/jsm/renderers/CSS2DRenderer.js';
import {normalizeUnit} from '../styles/conversions.js';
import {NumberNode, parseExpressions} from '../styles/parsers.js';
import {ModelScene} from './ModelScene.js';
export interface HotspotVisibilityDetails {
visible: boolean;
}
/**
* Hotspots are configured by slot name, and this name must begin with "hotspot"
* to be recognized. The position and normal strings are in the form of the
* camera-target attribute and default to "0m 0m 0m" and "0m 1m 0m",
* respectively.
*/
export interface HotspotConfiguration {
name: string;
position?: string;
normal?: string;
surface?: string;
}
const a = new Vector3();
const b = new Vector3();
const c = new Vector3();
const mat = new Matrix3();
const triangle = new Triangle();
const quat = new Quaternion();
/**
* The Hotspot object is a reference-counted slot. If decrement() returns true,
* it should be removed from the tree so it can be garbage-collected.
*/
export class Hotspot extends CSS2DObject {
public normal: Vector3 = new Vector3(0, 1, 0);
public surface?: string;
public mesh?: Mesh;
public tri?: Vector3;
public bary?: Vector3;
private initialized = false;
private referenceCount = 1;
private pivot = document.createElement('div');
private slot: HTMLSlotElement = document.createElement('slot');
constructor(config: HotspotConfiguration) {
super(document.createElement('div'));
this.element.classList.add('annotation-wrapper');
this.slot.name = config.name;
this.element.appendChild(this.pivot);
this.pivot.appendChild(this.slot);
this.updatePosition(config.position);
this.updateNormal(config.normal);
this.surface = config.surface;
}
get facingCamera(): boolean {
return !this.element.classList.contains('hide');
}
/**
* Sets the hotspot to be in the highly visible foreground state.
*/
show() {
if (!this.facingCamera || !this.initialized) {
this.updateVisibility(true);
}
}
/**
* Sets the hotspot to be in the diminished background state.
*/
hide() {
if (this.facingCamera || !this.initialized) {
this.updateVisibility(false);
}
}
/**
* Call this when adding elements to the same slot to keep track.
*/
increment() {
this.referenceCount++;
}
/**
* Call this when removing elements from the slot; returns true when the slot
* is unused.
*/
decrement(): boolean {
if (this.referenceCount > 0) {
--this.referenceCount;
}
return this.referenceCount === 0;
}
/**
* Change the position of the hotspot to the input string, in the same format
* as the data-position attribute.
*/
updatePosition(position?: string) {
if (position == null)
return;
const positionNodes = parseExpressions(position)[0].terms;
for (let i = 0; i < 3; ++i) {
this.position.setComponent(
i, normalizeUnit(positionNodes[i] as NumberNode<'m'>).number);
}
this.updateMatrixWorld();
}
/**
* Change the hotspot's normal to the input string, in the same format as the
* data-normal attribute.
*/
updateNormal(normal?: string) {
if (normal == null)
return;
const normalNodes = parseExpressions(normal)[0].terms;
for (let i = 0; i < 3; ++i) {
this.normal.setComponent(i, (normalNodes[i] as NumberNode).number);
}
}
updateSurface() {
const {mesh, tri, bary} = this;
if (mesh == null || tri == null || bary == null) {
return;
}
(mesh as any).getVertexPosition(tri.x, a);
(mesh as any).getVertexPosition(tri.y, b);
(mesh as any).getVertexPosition(tri.z, c);
a.toArray(mat.elements, 0);
b.toArray(mat.elements, 3);
c.toArray(mat.elements, 6);
this.position.copy(bary).applyMatrix3(mat);
const target = this.parent!;
target.worldToLocal(mesh.localToWorld(this.position));
triangle.set(a, b, c);
triangle.getNormal(this.normal).transformDirection(mesh.matrixWorld);
const pivot = target.parent as ModelScene;
quat.setFromAxisAngle(a.set(0, 1, 0), -pivot.rotation.y);
this.normal.applyQuaternion(quat);
}
orient(radians: number) {
this.pivot.style.transform = `rotate(${radians}rad)`;
}
updateVisibility(show: boolean) {
this.element.classList.toggle('hide', !show);
// NOTE: ShadyDOM doesn't support slot.assignedElements, otherwise we could
// use that here.
this.slot.assignedNodes().forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
const element = node as HTMLElement;
// Visibility attribute can be configured per-node in the hotspot:
const visibilityAttribute = element.dataset.visibilityAttribute;
if (visibilityAttribute != null) {
const attributeName = `data-${visibilityAttribute}`;
element.toggleAttribute(attributeName, show);
}
element.dispatchEvent(new CustomEvent('hotspot-visibility', {
detail: {
visible: show,
},
}));
});
this.initialized = true;
}
}