cione-comp-lib
Version:
`$ yarn add cione-comp-lib` 或者 `$ npm i cione-comp-lib -S`
691 lines (690 loc) • 23 kB
JavaScript
import { Vector3, Raycaster, Vector2, Scene, PerspectiveCamera, Object3D, Box3, Sphere, AnimationMixer, Euler, LoopPingPong, LoopRepeat, Matrix3, Triangle } from "../../../../_three@0.150.1@three/build/three.module.mjs";
import { CSS2DRenderer } from "../../../../_three@0.150.1@three/examples/jsm/renderers/CSS2DRenderer.mjs";
import { reduceVertices } from "../../../../_three@0.150.1@three/examples/jsm/utils/SceneUtils.mjs";
import { $currentGLTF, $originalGltfJson, $model } from "../features/scene-graph.mjs";
import { $nodeFromPoint, $nodeFromIndex } from "../features/scene-graph/model.mjs";
import { $renderer } from "../model-viewer-base.mjs";
import { normalizeUnit } from "../styles/conversions.mjs";
import { parseExpressions } from "../styles/parsers.mjs";
import { resolveDpr } from "../utilities.mjs";
import { Damper, SETTLING_TIME } from "./Damper.mjs";
import { Hotspot } from "./Hotspot.mjs";
import { Shadow } from "./Shadow.mjs";
/* @license
* Copyright 2019 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.
*/
const MIN_SHADOW_RATIO = 100;
const view = new Vector3();
const target = new Vector3();
const normalWorld = new Vector3();
const raycaster = new Raycaster();
const vector3 = new Vector3();
const ndc = new Vector2();
class ModelScene extends Scene {
constructor({ canvas, element, width, height }) {
super();
this.annotationRenderer = new CSS2DRenderer();
this.schemaElement = document.createElement("script");
this.width = 1;
this.height = 1;
this.aspect = 1;
this.scaleStep = 0;
this.renderCount = 0;
this.externalRenderer = null;
this.camera = new PerspectiveCamera(45, 1, 0.1, 100);
this.xrCamera = null;
this.url = null;
this.target = new Object3D();
this.animationNames = [];
this.boundingBox = new Box3();
this.boundingSphere = new Sphere();
this.size = new Vector3();
this.idealAspect = 0;
this.framedFoVDeg = 0;
this.shadow = null;
this.shadowIntensity = 0;
this.shadowSoftness = 1;
this.bakedShadows = /* @__PURE__ */ new Set();
this.exposure = 1;
this.canScale = true;
this.isDirty = false;
this.goalTarget = new Vector3();
this.targetDamperX = new Damper();
this.targetDamperY = new Damper();
this.targetDamperZ = new Damper();
this._currentGLTF = null;
this._model = null;
this.cancelPendingSourceChange = null;
this.animationsByName = /* @__PURE__ */ new Map();
this.currentAnimationAction = null;
this.name = "ModelScene";
this.element = element;
this.canvas = canvas;
this.camera = new PerspectiveCamera(45, 1, 0.1, 100);
this.camera.name = "MainCamera";
this.add(this.target);
this.setSize(width, height);
this.target.name = "Target";
this.mixer = new AnimationMixer(this.target);
const { domElement } = this.annotationRenderer;
const { style } = domElement;
style.display = "none";
style.pointerEvents = "none";
style.position = "absolute";
style.top = "0";
this.element.shadowRoot.querySelector(".default").appendChild(domElement);
this.schemaElement.setAttribute("type", "application/ld+json");
}
get context() {
return this.canvas.getContext("2d");
}
getCamera() {
return this.xrCamera != null ? this.xrCamera : this.camera;
}
queueRender() {
this.isDirty = true;
}
shouldRender() {
return this.isDirty;
}
hasRendered() {
this.isDirty = false;
}
forceRescale() {
this.scaleStep = -1;
this.queueRender();
}
async setObject(model) {
this.reset();
this._model = model;
this.target.add(model);
await this.setupScene();
}
async setSource(url, progressCallback = () => {
}) {
if (!url || url === this.url) {
progressCallback(1);
return;
}
this.reset();
this.url = url;
if (this.externalRenderer != null) {
const framingInfo = await this.externalRenderer.load(progressCallback);
this.boundingSphere.radius = framingInfo.framedRadius;
this.idealAspect = framingInfo.fieldOfViewAspect;
return;
}
if (this.cancelPendingSourceChange != null) {
this.cancelPendingSourceChange();
this.cancelPendingSourceChange = null;
}
let gltf;
try {
gltf = await new Promise(async (resolve, reject) => {
this.cancelPendingSourceChange = () => reject();
try {
const result = await this.element[$renderer].loader.load(url, this.element, progressCallback);
resolve(result);
} catch (error) {
reject(error);
}
});
} catch (error) {
if (error == null) {
return;
}
throw error;
}
this.cancelPendingSourceChange = null;
this.reset();
this.url = url;
this._currentGLTF = gltf;
if (gltf != null) {
this._model = gltf.scene;
this.target.add(gltf.scene);
}
const { animations } = gltf;
const animationsByName = /* @__PURE__ */ new Map();
const animationNames = [];
for (const animation of animations) {
animationsByName.set(animation.name, animation);
animationNames.push(animation.name);
}
this.animations = animations;
this.animationsByName = animationsByName;
this.animationNames = animationNames;
await this.setupScene();
}
async setupScene() {
this.applyTransform();
this.updateBoundingBox();
await this.updateFraming();
this.updateShadow();
this.setShadowIntensity(this.shadowIntensity);
}
reset() {
this.url = null;
this.renderCount = 0;
this.queueRender();
if (this.shadow != null) {
this.shadow.setIntensity(0);
}
this.bakedShadows.clear();
const { _model } = this;
if (_model != null) {
_model.removeFromParent();
this._model = null;
}
const gltf = this._currentGLTF;
if (gltf != null) {
gltf.dispose();
this._currentGLTF = null;
}
if (this.currentAnimationAction != null) {
this.currentAnimationAction.stop();
this.currentAnimationAction = null;
}
this.mixer.stopAllAction();
this.mixer.uncacheRoot(this);
}
dispose() {
this.reset();
if (this.shadow != null) {
this.shadow.dispose();
this.shadow = null;
}
this.element[$currentGLTF] = null;
this.element[$originalGltfJson] = null;
this.element[$model] = null;
}
get currentGLTF() {
return this._currentGLTF;
}
setSize(width, height) {
if (this.width === width && this.height === height) {
return;
}
this.width = Math.max(width, 1);
this.height = Math.max(height, 1);
this.annotationRenderer.setSize(width, height);
this.aspect = this.width / this.height;
if (this.externalRenderer != null) {
const dpr = resolveDpr();
this.externalRenderer.resize(width * dpr, height * dpr);
}
this.queueRender();
}
markBakedShadow(mesh) {
mesh.userData.shadow = true;
this.bakedShadows.add(mesh);
}
unmarkBakedShadow(mesh) {
mesh.userData.shadow = false;
mesh.visible = true;
this.bakedShadows.delete(mesh);
this.boundingBox.expandByObject(mesh);
}
findBakedShadows(group) {
const boundingBox = new Box3();
group.traverse((object) => {
const mesh = object;
if (!mesh.isMesh) {
return;
}
const material = mesh.material;
if (!material.transparent) {
return;
}
boundingBox.setFromObject(mesh);
const size = boundingBox.getSize(vector3);
const minDim = Math.min(size.x, size.y, size.z);
const maxDim = Math.max(size.x, size.y, size.z);
if (maxDim < MIN_SHADOW_RATIO * minDim) {
return;
}
this.markBakedShadow(mesh);
});
}
checkBakedShadows() {
const { min, max } = this.boundingBox;
const shadowBox = new Box3();
this.boundingBox.getSize(this.size);
for (const mesh of this.bakedShadows) {
shadowBox.setFromObject(mesh);
if (shadowBox.min.y < min.y + this.size.y / MIN_SHADOW_RATIO && shadowBox.min.x <= min.x && shadowBox.max.x >= max.x && shadowBox.min.z <= min.z && shadowBox.max.z >= max.z) {
continue;
}
if (shadowBox.min.z < min.z + this.size.z / MIN_SHADOW_RATIO && shadowBox.min.x <= min.x && shadowBox.max.x >= max.x && shadowBox.min.y <= min.y && shadowBox.max.y >= max.y) {
continue;
}
this.unmarkBakedShadow(mesh);
}
}
applyTransform() {
const { model } = this;
if (model == null) {
return;
}
const orientation = parseExpressions(this.element.orientation)[0].terms;
const roll = normalizeUnit(orientation[0]).number;
const pitch = normalizeUnit(orientation[1]).number;
const yaw = normalizeUnit(orientation[2]).number;
model.quaternion.setFromEuler(new Euler(pitch, yaw, roll, "YXZ"));
const scale = parseExpressions(this.element.scale)[0].terms;
model.scale.set(scale[0].number, scale[1].number, scale[2].number);
}
updateBoundingBox() {
const { model } = this;
if (model == null) {
return;
}
this.target.remove(model);
this.findBakedShadows(model);
const bound = (box, vertex) => {
return box.expandByPoint(vertex);
};
this.setBakedShadowVisibility(false);
this.boundingBox = reduceVertices(model, bound, new Box3());
if (this.boundingBox.isEmpty()) {
this.setBakedShadowVisibility(true);
this.bakedShadows.forEach((mesh) => this.unmarkBakedShadow(mesh));
this.boundingBox = reduceVertices(model, bound, new Box3());
}
this.checkBakedShadows();
this.setBakedShadowVisibility();
this.boundingBox.getSize(this.size);
this.target.add(model);
}
async updateFraming() {
const { model } = this;
if (model == null) {
return;
}
this.target.remove(model);
this.setBakedShadowVisibility(false);
const { center } = this.boundingSphere;
this.element.requestUpdate("cameraTarget");
await this.element.updateComplete;
center.copy(this.getTarget());
const radiusSquared = (value, vertex) => {
return Math.max(value, center.distanceToSquared(vertex));
};
this.boundingSphere.radius = Math.sqrt(reduceVertices(model, radiusSquared, 0));
const horizontalTanFov = (value, vertex) => {
vertex.sub(center);
const radiusXZ = Math.sqrt(vertex.x * vertex.x + vertex.z * vertex.z);
return Math.max(value, radiusXZ / (this.idealCameraDistance() - Math.abs(vertex.y)));
};
this.idealAspect = reduceVertices(model, horizontalTanFov, 0) / Math.tan(this.framedFoVDeg / 2 * Math.PI / 180);
this.setBakedShadowVisibility();
this.target.add(model);
}
setBakedShadowVisibility(visible = this.shadowIntensity <= 0) {
for (const shadow of this.bakedShadows) {
shadow.visible = visible;
}
}
idealCameraDistance() {
const halfFovRad = this.framedFoVDeg / 2 * Math.PI / 180;
return this.boundingSphere.radius / Math.sin(halfFovRad);
}
adjustedFoV(fovDeg) {
const vertical = Math.tan(fovDeg / 2 * Math.PI / 180) * Math.max(1, this.idealAspect / this.aspect);
return 2 * Math.atan(vertical) * 180 / Math.PI;
}
getNDC(clientX, clientY) {
if (this.xrCamera != null) {
ndc.set(clientX / window.screen.width, clientY / window.screen.height);
} else {
const rect = this.element.getBoundingClientRect();
ndc.set((clientX - rect.x) / this.width, (clientY - rect.y) / this.height);
}
ndc.multiplyScalar(2).subScalar(1);
ndc.y *= -1;
return ndc;
}
getSize() {
return { width: this.width, height: this.height };
}
setEnvironmentAndSkybox(environment, skybox) {
if (this.element[$renderer].arRenderer.presentedScene === this) {
return;
}
this.environment = environment;
this.background = skybox;
this.queueRender();
}
setTarget(modelX, modelY, modelZ) {
this.goalTarget.set(-modelX, -modelY, -modelZ);
}
setTargetDamperDecayTime(decayMilliseconds) {
this.targetDamperX.setDecayTime(decayMilliseconds);
this.targetDamperY.setDecayTime(decayMilliseconds);
this.targetDamperZ.setDecayTime(decayMilliseconds);
}
getTarget() {
return this.goalTarget.clone().multiplyScalar(-1);
}
jumpToGoal() {
this.updateTarget(SETTLING_TIME);
}
updateTarget(delta) {
const goal = this.goalTarget;
const target2 = this.target.position;
if (!goal.equals(target2)) {
const normalization = this.boundingSphere.radius / 10;
let { x, y, z } = target2;
x = this.targetDamperX.update(x, goal.x, delta, normalization);
y = this.targetDamperY.update(y, goal.y, delta, normalization);
z = this.targetDamperZ.update(z, goal.z, delta, normalization);
this.target.position.set(x, y, z);
this.target.updateMatrixWorld();
this.queueRender();
return true;
} else {
return false;
}
}
pointTowards(worldX, worldZ) {
const { x, z } = this.position;
this.yaw = Math.atan2(worldX - x, worldZ - z);
}
get model() {
return this._model;
}
set yaw(radiansY) {
this.rotation.y = radiansY;
this.queueRender();
}
get yaw() {
return this.rotation.y;
}
set animationTime(value) {
this.mixer.setTime(value);
this.queueShadowRender();
}
get animationTime() {
if (this.currentAnimationAction != null) {
const loopCount = Math.max(this.currentAnimationAction._loopCount, 0);
if (this.currentAnimationAction.loop === LoopPingPong && (loopCount & 1) === 1) {
return this.duration - this.currentAnimationAction.time;
} else {
return this.currentAnimationAction.time;
}
}
return 0;
}
set animationTimeScale(value) {
this.mixer.timeScale = value;
}
get animationTimeScale() {
return this.mixer.timeScale;
}
get duration() {
if (this.currentAnimationAction != null && this.currentAnimationAction.getClip()) {
return this.currentAnimationAction.getClip().duration;
}
return 0;
}
get hasActiveAnimation() {
return this.currentAnimationAction != null;
}
playAnimation(name = null, crossfadeTime = 0, loopMode = LoopRepeat, repetitionCount = Infinity) {
if (this._currentGLTF == null) {
return;
}
const { animations } = this;
if (animations == null || animations.length === 0) {
console.warn(`Cannot play animation (model does not have any animations)`);
return;
}
let animationClip = null;
if (name != null) {
animationClip = this.animationsByName.get(name);
if (animationClip == null) {
const parsedAnimationIndex = parseInt(name);
if (!isNaN(parsedAnimationIndex) && parsedAnimationIndex >= 0 && parsedAnimationIndex < animations.length) {
animationClip = animations[parsedAnimationIndex];
}
}
}
if (animationClip == null) {
animationClip = animations[0];
}
try {
const { currentAnimationAction: lastAnimationAction } = this;
const action = this.mixer.clipAction(animationClip, this);
this.currentAnimationAction = action;
if (this.element.paused) {
this.mixer.stopAllAction();
} else {
action.paused = false;
if (lastAnimationAction != null && action !== lastAnimationAction) {
action.crossFadeFrom(lastAnimationAction, crossfadeTime, false);
} else if (this.animationTimeScale > 0 && this.animationTime == this.duration) {
this.animationTime = 0;
}
}
action.setLoop(loopMode, repetitionCount);
action.enabled = true;
action.clampWhenFinished = true;
action.play();
} catch (error) {
console.error(error);
}
}
stopAnimation() {
this.currentAnimationAction = null;
this.mixer.stopAllAction();
}
updateAnimation(step) {
this.mixer.update(step);
this.queueShadowRender();
}
subscribeMixerEvent(event, callback) {
this.mixer.addEventListener(event, callback);
}
updateShadow() {
const shadow = this.shadow;
if (shadow != null) {
const side = this.element.arPlacement === "wall" ? "back" : "bottom";
shadow.setScene(this, this.shadowSoftness, side);
shadow.needsUpdate = true;
}
}
renderShadow(renderer) {
const shadow = this.shadow;
if (shadow != null && shadow.needsUpdate == true) {
shadow.render(renderer, this);
shadow.needsUpdate = false;
}
}
queueShadowRender() {
if (this.shadow != null) {
this.shadow.needsUpdate = true;
}
}
setShadowIntensity(shadowIntensity) {
this.shadowIntensity = shadowIntensity;
if (this._currentGLTF == null) {
return;
}
this.setBakedShadowVisibility();
if (shadowIntensity <= 0 && this.shadow == null) {
return;
}
if (this.shadow == null) {
const side = this.element.arPlacement === "wall" ? "back" : "bottom";
this.shadow = new Shadow(this, this.shadowSoftness, side);
}
this.shadow.setIntensity(shadowIntensity);
}
setShadowSoftness(softness) {
this.shadowSoftness = softness;
const shadow = this.shadow;
if (shadow != null) {
shadow.setSoftness(softness);
}
}
setShadowOffset(offset) {
const shadow = this.shadow;
if (shadow != null) {
shadow.setOffset(offset);
}
}
get raycaster() {
return raycaster;
}
hitFromPoint(ndcPosition, object = this) {
this.raycaster.setFromCamera(ndcPosition, this.getCamera());
const hits = this.raycaster.intersectObject(object, true);
return hits.find((hit) => hit.object.visible && !hit.object.userData.shadow);
}
positionAndNormalFromPoint(ndcPosition, object = this) {
const hit = this.hitFromPoint(ndcPosition, object);
if (hit == null || hit.face == null) {
return null;
}
if (hit.uv == null) {
return { position: hit.point, normal: hit.face.normal, uv: null };
}
hit.face.normal.applyNormalMatrix(new Matrix3().getNormalMatrix(hit.object.matrixWorld));
return { position: hit.point, normal: hit.face.normal, uv: hit.uv };
}
surfaceFromPoint(ndcPosition, object = this) {
const model = this.element.model;
if (model == null) {
return null;
}
const hit = this.hitFromPoint(ndcPosition, object);
if (hit == null || hit.face == null) {
return null;
}
const node = model[$nodeFromPoint](hit);
const { meshes, primitives } = node.mesh.userData.associations;
const va = new Vector3();
const vb = new Vector3();
const vc = new Vector3();
const { a, b, c } = hit.face;
const mesh = hit.object;
mesh.getVertexPosition(a, va);
mesh.getVertexPosition(b, vb);
mesh.getVertexPosition(c, vc);
const tri = new Triangle(va, vb, vc);
const uvw = new Vector3();
tri.getBarycoord(mesh.worldToLocal(hit.point), uvw);
return `${meshes} ${primitives} ${a} ${b} ${c} ${uvw.x.toFixed(3)} ${uvw.y.toFixed(3)} ${uvw.z.toFixed(3)}`;
}
addHotspot(hotspot) {
this.target.add(hotspot);
this.annotationRenderer.domElement.appendChild(hotspot.element);
}
removeHotspot(hotspot) {
this.target.remove(hotspot);
}
forHotspots(func) {
const { children } = this.target;
for (let i = 0, l = children.length; i < l; i++) {
const hotspot = children[i];
if (hotspot instanceof Hotspot) {
func(hotspot);
}
}
}
initializeSurface(hotspot) {
if (hotspot.surface != null && hotspot.mesh == null) {
const nodes = parseExpressions(hotspot.surface)[0].terms;
if (nodes.length != 8) {
console.warn(hotspot.surface + " does not have exactly 8 numbers.");
return;
}
const primitiveNode = this.element.model[$nodeFromIndex](nodes[0].number, nodes[1].number);
const tri = new Vector3(nodes[2].number, nodes[3].number, nodes[4].number);
if (primitiveNode == null) {
console.warn(hotspot.surface + " does not match a node/primitive in this glTF! Skipping this hotspot.");
return;
}
const numVert = primitiveNode.mesh.geometry.attributes.position.count;
if (tri.x >= numVert || tri.y >= numVert || tri.z >= numVert) {
console.warn(hotspot.surface + " vertex indices out of range in this glTF! Skipping this hotspot.");
return;
}
const bary = new Vector3(nodes[5].number, nodes[6].number, nodes[7].number);
hotspot.mesh = primitiveNode.mesh;
hotspot.tri = tri;
hotspot.bary = bary;
}
}
updateSurfaceHotspots() {
const forceUpdate = !this.element.paused;
this.forHotspots((hotspot) => {
this.initializeSurface(hotspot);
hotspot.updateSurface(forceUpdate);
});
}
updateHotspotsVisibility(viewerPosition) {
this.forHotspots((hotspot) => {
view.copy(viewerPosition);
target.setFromMatrixPosition(hotspot.matrixWorld);
view.sub(target);
normalWorld.copy(hotspot.normal).transformDirection(this.target.matrixWorld);
if (view.dot(normalWorld) < 0) {
hotspot.hide();
} else {
hotspot.show();
}
});
}
orientHotspots(radians) {
this.forHotspots((hotspot) => {
hotspot.orient(radians);
});
}
setHotspotsVisibility(visible) {
this.forHotspots((hotspot) => {
hotspot.visible = visible;
});
}
updateSchema(src) {
var _a;
const { schemaElement, element } = this;
const { alt, poster, iosSrc } = element;
if (src != null) {
const encoding = [{
"@type": "MediaObject",
contentUrl: src,
encodingFormat: ((_a = src.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === "gltf" ? "model/gltf+json" : "model/gltf-binary"
}];
if (iosSrc) {
encoding.push({
"@type": "MediaObject",
contentUrl: iosSrc,
encodingFormat: "model/vnd.usdz+zip"
});
}
const structuredData = {
"@context": "http://schema.org/",
"@type": "3DModel",
image: poster !== null && poster !== void 0 ? poster : void 0,
name: alt !== null && alt !== void 0 ? alt : void 0,
encoding
};
schemaElement.textContent = JSON.stringify(structuredData);
document.head.appendChild(schemaElement);
} else if (schemaElement.parentElement != null) {
schemaElement.parentElement.removeChild(schemaElement);
}
}
}
export { ModelScene };