diff --git a/services/static-webserver/client/source/class/osparc/Application.js b/services/static-webserver/client/source/class/osparc/Application.js
index 67ef7ac7614..10af5ae1292 100644
--- a/services/static-webserver/client/source/class/osparc/Application.js
+++ b/services/static-webserver/client/source/class/osparc/Application.js
@@ -64,6 +64,9 @@ qx.Class.define("osparc.Application", {
const intlTelInput = osparc.wrapper.IntlTelInput.getInstance();
intlTelInput.init();
+ const threejs = osparc.wrapper.Three.getInstance();
+ threejs.init();
+
const webSocket = osparc.wrapper.WebSocket.getInstance();
webSocket.addListener("connect", () => osparc.io.WatchDog.getInstance().setOnline(true));
webSocket.addListener("disconnect", () => osparc.io.WatchDog.getInstance().setOnline(false));
diff --git a/services/static-webserver/client/source/class/osparc/component/editor/ThumbnailSuggestions.js b/services/static-webserver/client/source/class/osparc/component/editor/ThumbnailSuggestions.js
index 673997eacc2..08fb7de2454 100644
--- a/services/static-webserver/client/source/class/osparc/component/editor/ThumbnailSuggestions.js
+++ b/services/static-webserver/client/source/class/osparc/component/editor/ThumbnailSuggestions.js
@@ -76,8 +76,9 @@ qx.Class.define("osparc.component.editor.ThumbnailSuggestions", {
this.__thumbnailsPerNode[nodeId] = [];
}
this.__thumbnailsPerNode[nodeId].push({
- type: "image",
- source: srvMetadata["thumbnail"]
+ type: "serviceImage",
+ thumbnailUrl: srvMetadata["thumbnail"],
+ fileUrl: srvMetadata["thumbnail"]
});
}
});
@@ -87,12 +88,33 @@ qx.Class.define("osparc.component.editor.ThumbnailSuggestions", {
// make it first in the list
this.__thumbnailsPerNode["0000-workbenchUIPreview"] = [{
type: "workbenchUIPreview",
- source: osparc.product.Utils.getWorkbenhUIPreviewPath()
+ thumbnailUrl: osparc.product.Utils.getWorkbenhUIPreviewPath(),
+ fileUrl: osparc.product.Utils.getWorkbenhUIPreviewPath()
}];
const themeManager = qx.theme.manager.Meta.getInstance();
themeManager.addListener("changeTheme", () => this.addWorkbenchUIPreviewToSuggestions());
},
+ addPreviewsToSuggestions: function(previewsPerNodes) {
+ previewsPerNodes.forEach(previewsPerNode => {
+ const nodeId = previewsPerNode["node_id"];
+ const previews = previewsPerNode["screenshots"];
+ if (previews && previews.length) {
+ if (!(nodeId in this.__thumbnailsPerNode)) {
+ this.__thumbnailsPerNode[nodeId] = [];
+ }
+ previews.forEach(preview => {
+ this.__thumbnailsPerNode[nodeId].push({
+ type: preview["mimetype"],
+ thumbnailUrl: preview["thumbnail_url"],
+ fileUrl: preview["file_url"]
+ });
+ });
+ }
+ });
+ this.setSelectedNodeId(null);
+ },
+
setSelectedNodeId: function(selectedNodeId) {
let suggestions = new Set([]);
if (selectedNodeId && selectedNodeId in this.__thumbnailsPerNode) {
@@ -111,14 +133,14 @@ qx.Class.define("osparc.component.editor.ThumbnailSuggestions", {
this.removeAll();
suggestions.forEach(suggestion => {
const maxHeight = this.getMaxHeight();
- const thumbnail = new osparc.ui.basic.Thumbnail(suggestion.source, maxHeight, parseInt(maxHeight*2/3));
+ const thumbnail = new osparc.ui.basic.Thumbnail(suggestion.thumbnailUrl, maxHeight, parseInt(maxHeight*2/3));
thumbnail.setMarginLeft(1); // give some extra space to the selection border
thumbnail.addListener("tap", () => {
this.getChildren().forEach(thumbnailImg => osparc.utils.Utils.removeBorder(thumbnailImg));
osparc.utils.Utils.addBorder(thumbnail, 1, "#007fd4"); // Visual Studio blue
this.fireDataEvent("thumbnailTapped", {
type: suggestion.type,
- source: suggestion.source
+ source: suggestion.fileUrl
});
}, this);
this.add(thumbnail);
diff --git a/services/static-webserver/client/source/class/osparc/component/widget/Three.js b/services/static-webserver/client/source/class/osparc/component/widget/Three.js
new file mode 100644
index 00000000000..6b4530faa61
--- /dev/null
+++ b/services/static-webserver/client/source/class/osparc/component/widget/Three.js
@@ -0,0 +1,122 @@
+/* ************************************************************************
+
+ osparc - the simcore frontend
+
+ https://osparc.io
+
+ Copyright:
+ 2023 IT'IS Foundation, https://itis.swiss
+
+ License:
+ MIT: https://opensource.org/licenses/MIT
+
+ Authors:
+ * Odei Maiz (odeimaiz)
+
+************************************************************************ */
+
+qx.Class.define("osparc.component.widget.Three", {
+ extend: qx.ui.core.Widget,
+
+ construct : function(fileUrl) {
+ this.base(arguments);
+
+ this._setLayout(new qx.ui.layout.Canvas());
+
+ this.__transformControls = [];
+ this.__entities = [];
+
+ this.addListenerOnce("appear", () => {
+ this.__threeWrapper = osparc.wrapper.Three.getInstance();
+ if (this.__threeWrapper.isLibReady()) {
+ this.__start(fileUrl);
+ } else {
+ this.__threeWrapper.addListener("ThreeLibReady", e => {
+ if (e.getData()) {
+ this.__start(fileUrl);
+ }
+ });
+ }
+ }, this);
+ },
+
+ members: {
+ __threeWrapper: null,
+
+ __start: function(fileUrl) {
+ this.getContentElement().getDomElement()
+ .appendChild(this.__threeWrapper.getDomElement());
+
+ // this.__threeWrapper.SetCameraPosition(18, 0, 25);
+ this.__threeWrapper.setCameraPosition(210, 210, 90); // Z up
+ this.__threeWrapper.setBackgroundColor("#484f54");
+ this.__resized();
+
+ this.addListener("resize", () => this.__resized(), this);
+
+ this.__render();
+
+ this.__threeWrapper.loadScene(fileUrl);
+ },
+
+ __resized: function() {
+ const minWidth = 400;
+ const minHeight = 400;
+ const bounds = this.getBounds();
+ const width = Math.max(minWidth, bounds.width);
+ const height = Math.max(minHeight, bounds.height);
+ this.__threeWrapper.setSize(width, height);
+ },
+
+ getThreeWrapper: function() {
+ return this.__threeWrapper;
+ },
+
+ __render: function() {
+ this.__threeWrapper.render();
+ },
+
+ addSnappingPlane: function(fixedAxe = 2, fixedPosition = 0) {
+ let instersectionPlane = this.__threeWrapper.createInvisiblePlane(fixedAxe, fixedPosition);
+ instersectionPlane.name = "PlaneForSnapping";
+ this.__entities.push(instersectionPlane);
+ },
+
+ removeSnappingPlane: function() {
+ for (let i = 0; i < this.__entities.length; i++) {
+ if (this.__entities[i].name === "PlaneForSnapping") {
+ this.__entities.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ centerCameraToBB: function() {
+ let center = {
+ x: 0,
+ y: 0,
+ z: 0
+ };
+ if (this.__entities.length > 0) {
+ let unionBBox = null;
+ for (let i = 0; i < this.__entities.length; i++) {
+ const ent = this.__entities[i];
+ if (ent.Name === "PlaneForSnapping") {
+ continue;
+ }
+ const bBox = this.__threeWrapper.getBBox(ent);
+ if (unionBBox === null) {
+ unionBBox = bBox;
+ }
+ unionBBox = this.__threeWrapper.mergeBBoxes(bBox, unionBBox);
+ }
+ center = this.__threeWrapper.getBBoxCenter(unionBBox);
+ }
+ this.__threeWrapper.setOrbitPoint(center);
+ },
+
+ importGLTFSceneFromBuffer: function(modelBuffer) {
+ this.__threeWrapper.importGLTFSceneFromBuffer(modelBuffer);
+ }
+ }
+});
diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js
index 36200972e36..5edcaed5f94 100644
--- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js
+++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js
@@ -361,6 +361,16 @@ qx.Class.define("osparc.dashboard.ResourceMoreOptions", {
return page;
},
+ __getScenePage: function() {
+ const id = "Scene";
+ const title = this.tr("Scene");
+ const icon = "https://avatars.githubusercontent.com/u/33161876?s=32";
+ const threeView = new osparc.component.widget.Three("#00FF00");
+ const page = this.__permissionsPage = this.__createPage(title, threeView, icon, id);
+ page.setIcon(icon);
+ return page;
+ },
+
__getPermissionsPage: function() {
const id = "Permissions";
const resourceData = this.__resourceData;
diff --git a/services/static-webserver/client/source/class/osparc/dashboard/StudyThumbnailExplorer.js b/services/static-webserver/client/source/class/osparc/dashboard/StudyThumbnailExplorer.js
index f5292c47a33..5fbeb96dd0b 100644
--- a/services/static-webserver/client/source/class/osparc/dashboard/StudyThumbnailExplorer.js
+++ b/services/static-webserver/client/source/class/osparc/dashboard/StudyThumbnailExplorer.js
@@ -107,6 +107,16 @@ qx.Class.define("osparc.dashboard.StudyThumbnailExplorer", {
});
thumbnailSuggestions.addWorkbenchUIPreviewToSuggestions();
thumbnailSuggestions.setStudy(study);
+
+ const params = {
+ url: {
+ studyId: this.__studyData["uuid"]
+ }
+ };
+ osparc.data.Resources.fetch("studyPreviews", "getPreviews", params)
+ .then(previewsPerNodes => thumbnailSuggestions.addPreviewsToSuggestions(previewsPerNodes))
+ .catch(err => console.error(err));
+
return thumbnailSuggestions;
},
@@ -128,12 +138,15 @@ qx.Class.define("osparc.dashboard.StudyThumbnailExplorer", {
const thumbnailData = e.getData();
let control = null;
switch (thumbnailData["type"]) {
- case "image":
- control = this.__getThumbnail(thumbnailData["source"]);
- break;
case "workbenchUIPreview":
control = this.__getWorkbenchUIPreview();
break;
+ case null:
+ control = this.__getThreeSceneViewer(thumbnailData["source"]);
+ break;
+ default:
+ control = this.__getThumbnail(thumbnailData["source"]);
+ break;
}
if (control) {
thumbnailViewerLayout.removeAll();
@@ -171,6 +184,11 @@ qx.Class.define("osparc.dashboard.StudyThumbnailExplorer", {
return workbenchUIPreview;
},
+ __getThreeSceneViewer: function(fileUrl) {
+ const threeView = new osparc.component.widget.Three(fileUrl);
+ return threeView;
+ },
+
__initComponents: function() {
const scrollThumbnails = this.getChildControl("scroll-thumbnails");
scrollThumbnails.setSelectedNodeId(null);
diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js
index 9bc82054d58..f327c737bc3 100644
--- a/services/static-webserver/client/source/class/osparc/data/Resources.js
+++ b/services/static-webserver/client/source/class/osparc/data/Resources.js
@@ -199,6 +199,16 @@ qx.Class.define("osparc.data.Resources", {
}
}
},
+ "studyPreviews": {
+ useCache: true,
+ idField: "uuid",
+ endpoints: {
+ getPreviews: {
+ method: "GET",
+ url: statics.API + "/projects/{studyId}/nodes/-/preview"
+ }
+ }
+ },
/*
* NODES
*/
diff --git a/services/static-webserver/client/source/class/osparc/info/CardLarge.js b/services/static-webserver/client/source/class/osparc/info/CardLarge.js
index 47ceda420d6..97111ab4b1b 100644
--- a/services/static-webserver/client/source/class/osparc/info/CardLarge.js
+++ b/services/static-webserver/client/source/class/osparc/info/CardLarge.js
@@ -57,7 +57,8 @@ qx.Class.define("osparc.info.CardLarge", {
members: {
_attachHandlers: function() {
this.addListenerOnce("appear", () => this._rebuildLayout(), this);
- this.addListener("resize", () => this._rebuildLayout(), this);
+ // OM: Not so sure about this one
+ // this.addListener("resize", () => this._rebuildLayout(), this);
},
_rebuildLayout: function() {
diff --git a/services/static-webserver/client/source/class/osparc/product/Utils.js b/services/static-webserver/client/source/class/osparc/product/Utils.js
index d9ed592fd2a..7f615eff37c 100644
--- a/services/static-webserver/client/source/class/osparc/product/Utils.js
+++ b/services/static-webserver/client/source/class/osparc/product/Utils.js
@@ -145,7 +145,7 @@ qx.Class.define("osparc.product.Utils", {
},
showStudyPreview: function() {
- if (this.isProduct("osparc") || this.isProduct("s4l")) {
+ if (this.isProduct("osparc") || this.isProduct("s4l") || this.isProduct("s4llite")) {
return true;
}
return false;
diff --git a/services/static-webserver/client/source/class/osparc/store/Store.js b/services/static-webserver/client/source/class/osparc/store/Store.js
index 125a4eb3f9f..fcf3b5cd1e7 100644
--- a/services/static-webserver/client/source/class/osparc/store/Store.js
+++ b/services/static-webserver/client/source/class/osparc/store/Store.js
@@ -66,6 +66,10 @@ qx.Class.define("osparc.store.Store", {
check: "Array",
init: []
},
+ studyPreviews: {
+ check: "Array",
+ init: []
+ },
nodesInStudyResources: {
check: "Array",
init: []
diff --git a/services/static-webserver/client/source/class/osparc/wrapper/Three.js b/services/static-webserver/client/source/class/osparc/wrapper/Three.js
new file mode 100644
index 00000000000..5c2c7c0eea9
--- /dev/null
+++ b/services/static-webserver/client/source/class/osparc/wrapper/Three.js
@@ -0,0 +1,377 @@
+/* ************************************************************************
+
+ osparc - the simcore frontend
+
+ https://osparc.io
+
+ Copyright:
+ 2023 IT'IS Foundation, https://itis.swiss
+
+ License:
+ MIT: https://opensource.org/licenses/MIT
+
+ Authors:
+ * Odei Maiz (odeimaiz)
+
+************************************************************************ */
+
+/* global THREE */
+
+/**
+ * ignore THREE
+ * eslint new-cap: [2, {capIsNewExceptions: ["THREE", "Blob"]}]
+ * eslint no-underscore-dangle: ["error", { "allowAfterThis": true, "enforceInMethodNames": true }]
+ */
+
+/**
+ * @asset(threejs/*)
+ */
+
+/**
+ * A qooxdoo wrapper for
+ * Threejs
+ */
+
+qx.Class.define("osparc.wrapper.Three", {
+ extend: qx.core.Object,
+ type: "singleton",
+
+ statics: {
+ NAME: "three.js",
+ VERSION: "0.100.0",
+ URL: "https://github.com/mrdoob/three.js"
+ },
+
+ properties: {
+ libReady: {
+ check: "Boolean",
+ init: false,
+ nullable: false,
+ event: "changeLibReady",
+ apply: "__applyLibReady"
+ }
+ },
+ members: {
+ __scene: null,
+ __camera: null,
+ ___raycaster: null,
+ __renderer: null,
+ __orbitControls: null,
+ __mouse: null,
+
+ init: function() {
+ // initialize the script loading
+ const threePath = "threejs/three.min.js";
+ const orbitPath = "threejs/OrbitControls.js";
+ const gltfLoaderPath = "threejs/GLTFLoader.js";
+ const dynLoader = new qx.util.DynamicScriptLoader([
+ threePath,
+ orbitPath,
+ gltfLoaderPath
+ ]);
+
+ dynLoader.addListenerOnce("ready", () => {
+ console.log(threePath + " loaded");
+ this.setLibReady(true);
+ }, this);
+
+ dynLoader.addListener("failed", e => {
+ let data = e.getData();
+ console.error("failed to load " + data.script);
+ }, this);
+
+ dynLoader.start();
+ },
+
+ __applyLibReady: function(ready) {
+ if (ready) {
+ this.__scene = new THREE.Scene();
+
+ this.__camera = new THREE.PerspectiveCamera();
+ this.__camera.far = 10000;
+ this.__camera.up.set(0, 0, 1);
+ this.__scene.add(this.__camera);
+
+ this.__addCameraLight();
+ this.__addGridHelper();
+ this.__addAxesHelper();
+
+ this.__mouse = new THREE.Vector2();
+ this.__raycaster = new THREE.Raycaster();
+
+ this.__renderer = new THREE.WebGLRenderer();
+
+ this.__addOrbitControls();
+ this.render();
+ }
+ },
+
+ getDomElement: function() {
+ return this.__renderer.domElement;
+ },
+
+ render: function() {
+ this.__renderer.render(this.__scene, this.__camera);
+ },
+
+ addEntityToScene: function(objToScene) {
+ this.__scene.add(objToScene);
+ this.render();
+ },
+
+ importGLTFSceneFromBuffer: function(modelBuffer) {
+ let scope = this;
+
+ const onLoad = myScene => {
+ console.log(myScene.scene);
+ for (let i = myScene.scene.children.length-1; i >=0; i--) {
+ if (myScene.scene.children[i].type === "Mesh" ||
+ myScene.scene.children[i].type === "Line") {
+ // Not really sure about this
+ myScene.scene.children[i].uuid = modelBuffer.uuid;
+ const data = {
+ name: modelBuffer.name,
+ pathNames: modelBuffer.pathNames,
+ pathUuids: modelBuffer.pathUuids,
+ uuid: modelBuffer.uuid,
+ entity: myScene.scene.children[i]
+ };
+ scope.fireDataEvent("EntityToBeAdded", data);
+ } else {
+ console.log("Will not loaded", myScene.scene.children[i]);
+ }
+ }
+ };
+
+ const onError = err => console.error("GLTFLoader An error happened", err);
+
+ const glTFLoader = new THREE.GLTFLoader();
+ glTFLoader.parse(
+ modelBuffer.value,
+ null,
+ onLoad,
+ onError
+ );
+ },
+
+ loadScene: function(scenePath) {
+ const onLoad = gltf => {
+ this.__scene.add(gltf.scene);
+ /*
+ gltf.animations; // Array
+ gltf.scene; // THREE.Group
+ gltf.scenes; // Array
+ gltf.cameras; // Array
+ gltf.asset; // Object
+ */
+
+ // OM
+ this.__fitCameraToCenteredObject(gltf.scene.children);
+ };
+
+ const onProgress = xhr => console.log((xhr.loaded / xhr.total * 100) + "% loaded");
+
+ const onError = err => console.error("GLTFLoader An error happened", err);
+
+ const loader = new THREE.GLTFLoader();
+ // Load a glTF resource
+ loader.load(
+ // resource URL
+ scenePath,
+ // called when the resource is loaded
+ onLoad,
+ // called while loading is progressing
+ onProgress,
+ // called when loading has errors
+ onError
+ );
+ },
+
+ setBackgroundColor: function(backgroundColor) {
+ this.__scene.background = new THREE.Color(backgroundColor);
+ },
+
+ setCameraPosition: function(x = 0, y = 0, z = 0) {
+ this.__camera.position.x = x;
+ this.__camera.position.y = y;
+ this.__camera.position.z = z;
+ },
+
+ setSize: function(width, height) {
+ this.__renderer.setSize(width, height);
+ this.__camera.aspect = width / height;
+ this.__camera.updateProjectionMatrix();
+ this.__renderer.setSize(width, height);
+ this.__orbitControls.update();
+ this.render();
+ },
+
+ __fitCameraToCenteredObject: function(selection, fitOffset = 1.2) {
+ const camera = this.__camera;
+ const controls = this.__orbitControls;
+
+ const size = new THREE.Vector3();
+ const center = new THREE.Vector3();
+ const box = new THREE.Box3();
+ box.makeEmpty();
+ for (const object of selection) {
+ box.expandByObject(object);
+ }
+ box.getSize(size);
+ box.getCenter(center);
+
+ const maxSize = Math.max(size.x, size.y, size.z);
+ const fitHeightDistance = maxSize / (2 * Math.atan(Math.PI * camera.fov / 360));
+ const fitWidthDistance = fitHeightDistance / camera.aspect;
+ const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance);
+
+ const direction = controls.target.clone()
+ .sub(camera.position)
+ .normalize()
+ .multiplyScalar(distance);
+
+ controls.maxDistance = distance * 10;
+ controls.target.copy(center);
+
+ camera.near = distance / 100;
+ camera.far = distance * 100;
+ camera.updateProjectionMatrix();
+
+ camera.position.copy(controls.target).sub(direction);
+
+ controls.update();
+
+ this.render();
+ },
+
+ fromEntityMeshToEntity: function(entityMesh) {
+ let geom = new THREE.Geometry();
+ for (let i = 0; i < entityMesh.vertices.length; i+=3) {
+ let v1 = new THREE.Vector3(entityMesh.vertices[i+0], entityMesh.vertices[i+1], entityMesh.vertices[i+2]);
+ geom.vertices.push(v1);
+ }
+ for (let i = 0; i < entityMesh.triangles.length; i+=3) {
+ geom.faces.push(new THREE.Face3(entityMesh.triangles[i+0], entityMesh.triangles[i+1], entityMesh.triangles[i+2]));
+ }
+
+ geom.computeFaceNormals();
+ const applySmoothing = true;
+ if (applySmoothing) {
+ geom.mergeVertices();
+ geom.computeVertexNormals();
+ }
+
+ return geom;
+ },
+
+ fromEntityToEntityMesh: function(entity) {
+ let i = 0;
+ let j = 0;
+ let m = 0;
+ let myVertices = [];
+ if (entity.geometry.vertices) {
+ // Geometries
+ for (i = 0; i < entity.geometry.vertices.length; i++) {
+ myVertices.push(entity.geometry.vertices[i].x);
+ myVertices.push(entity.geometry.vertices[i].y);
+ myVertices.push(entity.geometry.vertices[i].z);
+ }
+ } else {
+ // BufferGeometries
+ let vertices = entity.geometry.getAttribute("position");
+ let vertex = new THREE.Vector3();
+ for (i = 0; i < vertices.count; i++) {
+ vertex.x = vertices.getX(i);
+ vertex.y = vertices.getY(i);
+ vertex.z = vertices.getZ(i);
+
+ // transfrom the vertex to world space
+ vertex.applyMatrix4(entity.matrixWorld);
+
+ myVertices.push(vertex.x);
+ myVertices.push(vertex.y);
+ myVertices.push(vertex.z);
+ }
+ }
+
+ let myFaces = [];
+ if (entity.geometry.faces) {
+ // Geometries
+ for (i = 0; i < entity.geometry.faces.length; i++) {
+ myFaces.push(entity.geometry.faces[i].a);
+ myFaces.push(entity.geometry.faces[i].b);
+ myFaces.push(entity.geometry.faces[i].c);
+ }
+ } else {
+ // BufferGeometries
+ let vertices = entity.geometry.getAttribute("position");
+ for (i = 0; i < vertices.count; i += 3) {
+ for (m = 0; m < 3; m++) {
+ j = i + m + 1;
+ // j = i + m;
+ myFaces.push(j);
+ }
+ }
+ }
+
+ let entityMesh = {
+ vertices: myVertices,
+ triangles: myFaces,
+ normals: [],
+ transform4x4: entity.matrix.elements,
+ material: null,
+ lines: [],
+ points: []
+ };
+
+ console.log(entityMesh.vertices);
+ console.log(entityMesh.triangles);
+ console.log(entityMesh.transform4x4);
+
+ return entityMesh;
+ },
+
+ __addCameraLight: function() {
+ const pointLight = new THREE.PointLight(0xFFFFFF);
+ pointLight.position.set(1, 1, 2);
+ this.__camera.add(pointLight);
+ },
+
+ __addGridHelper: function() {
+ const gridSize = 20;
+ const gridDivisions = 20;
+ const centerLineColor = new THREE.Color(0xFFFFFF);
+ const gridColor = new THREE.Color(0xEEEEEE);
+ let gridHelper = new THREE.GridHelper(gridSize, gridDivisions, centerLineColor, gridColor);
+ // Z up:
+ // https://stackoverflow.com/questions/44630265/how-can-i-set-z-up-coordinate-system-in-three-js
+ gridHelper.geometry.rotateX(Math.PI / 2);
+ let vector = new THREE.Vector3(0, 0, 1);
+ gridHelper.lookAt(vector);
+ gridHelper.name = "GridHelper";
+ this.__scene.add(gridHelper);
+ },
+
+ __addAxesHelper: function() {
+ let axes = new THREE.AxesHelper(1);
+ axes.name = "AxesHelper";
+ this.__scene.add(axes);
+ },
+
+ __addOrbitControls: function() {
+ this.__orbitControls = new THREE.OrbitControls(this.__camera, this.__renderer.domElement);
+ this.__orbitControls.addEventListener("change", this.__updateOrbitControls.bind(this));
+ this.__orbitControls.update();
+ },
+
+ setOrbitPoint: function(newPos) {
+ this.__orbitControls.target.set(newPos.x, newPos.y, newPos.z);
+ this.__orbitControls.update();
+ this.render();
+ },
+
+ __updateOrbitControls: function() {
+ this.render();
+ }
+ }
+});
diff --git a/services/static-webserver/client/source/resource/threejs/GLTFLoader.js b/services/static-webserver/client/source/resource/threejs/GLTFLoader.js
new file mode 100644
index 00000000000..9c999a31527
--- /dev/null
+++ b/services/static-webserver/client/source/resource/threejs/GLTFLoader.js
@@ -0,0 +1,2638 @@
+/**
+ * @author Rich Tibbett / https://github.com/richtr
+ * @author mrdoob / http://mrdoob.com/
+ * @author Tony Parisi / http://www.tonyparisi.com/
+ * @author Takahiro / https://github.com/takahirox
+ * @author Don McCurdy / https://www.donmccurdy.com
+ */
+
+THREE.GLTFLoader = ( function () {
+
+ function GLTFLoader( manager ) {
+
+ this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
+
+ }
+
+ GLTFLoader.prototype = {
+
+ constructor: GLTFLoader,
+
+ crossOrigin: 'Anonymous',
+
+ load: function ( url, onLoad, onProgress, onError ) {
+
+ var scope = this;
+
+ var path = this.path !== undefined ? this.path : THREE.LoaderUtils.extractUrlBase( url );
+
+ var loader = new THREE.FileLoader( scope.manager );
+
+ loader.setResponseType( 'arraybuffer' );
+
+ loader.load( url, function ( data ) {
+
+ try {
+
+ scope.parse( data, path, onLoad, onError );
+
+ } catch ( e ) {
+
+ if ( onError !== undefined ) {
+
+ onError( e );
+
+ } else {
+
+ throw e;
+
+ }
+
+ }
+
+ }, onProgress, onError );
+
+ },
+
+ setCrossOrigin: function ( value ) {
+
+ this.crossOrigin = value;
+ return this;
+
+ },
+
+ setPath: function ( value ) {
+
+ this.path = value;
+ return this;
+
+ },
+
+ parse: function ( data, path, onLoad, onError ) {
+
+ var content;
+ var extensions = {};
+
+ if ( typeof data === 'string' ) {
+
+ content = data;
+
+ } else {
+
+ var magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );
+
+ if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {
+
+ try {
+
+ extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data );
+
+ } catch ( error ) {
+
+ if ( onError ) onError( error );
+ return;
+
+ }
+
+ content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content;
+
+ } else {
+
+ content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) );
+
+ }
+
+ }
+
+ var json = JSON.parse( content );
+
+ if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) {
+
+ if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.' ) );
+ return;
+
+ }
+
+ if ( json.extensionsUsed ) {
+
+ if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_LIGHTS ) >= 0 ) {
+
+ extensions[ EXTENSIONS.KHR_LIGHTS ] = new GLTFLightsExtension( json );
+
+ }
+
+ if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_MATERIALS_COMMON ) >= 0 ) {
+
+ extensions[ EXTENSIONS.KHR_MATERIALS_COMMON ] = new GLTFMaterialsCommonExtension( json );
+
+ }
+
+ if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ) >= 0 ) {
+
+ extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] = new GLTFMaterialsPbrSpecularGlossinessExtension();
+
+ }
+
+ }
+
+ console.time( 'GLTFLoader' );
+
+ var parser = new GLTFParser( json, extensions, {
+
+ path: path || this.path || '',
+ crossOrigin: this.crossOrigin,
+ manager: this.manager
+
+ } );
+
+ parser.parse( function ( scene, scenes, cameras, animations, asset ) {
+
+ console.timeEnd( 'GLTFLoader' );
+
+ var glTF = {
+ scene: scene,
+ scenes: scenes,
+ cameras: cameras,
+ animations: animations,
+ asset: asset
+ };
+
+ onLoad( glTF );
+
+ }, onError );
+
+ }
+
+ };
+
+ /* GLTFREGISTRY */
+
+ function GLTFRegistry() {
+
+ var objects = {};
+
+ return {
+
+ get: function ( key ) {
+
+ return objects[ key ];
+
+ },
+
+ add: function ( key, object ) {
+
+ objects[ key ] = object;
+
+ },
+
+ remove: function ( key ) {
+
+ delete objects[ key ];
+
+ },
+
+ removeAll: function () {
+
+ objects = {};
+
+ }
+
+ };
+
+ }
+
+ /*********************************/
+ /********** EXTENSIONS ***********/
+ /*********************************/
+
+ var EXTENSIONS = {
+ KHR_BINARY_GLTF: 'KHR_binary_glTF',
+ KHR_LIGHTS: 'KHR_lights',
+ KHR_MATERIALS_COMMON: 'KHR_materials_common',
+ KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness'
+ };
+
+ /**
+ * Lights Extension
+ *
+ * Specification: PENDING
+ */
+ function GLTFLightsExtension( json ) {
+
+ this.name = EXTENSIONS.KHR_LIGHTS;
+
+ this.lights = {};
+
+ var extension = ( json.extensions && json.extensions[ EXTENSIONS.KHR_LIGHTS ] ) || {};
+ var lights = extension.lights || {};
+
+ for ( var lightId in lights ) {
+
+ var light = lights[ lightId ];
+ var lightNode;
+
+ var color = new THREE.Color().fromArray( light.color );
+
+ switch ( light.type ) {
+
+ case 'directional':
+ lightNode = new THREE.DirectionalLight( color );
+ lightNode.position.set( 0, 0, 1 );
+ break;
+
+ case 'point':
+ lightNode = new THREE.PointLight( color );
+ break;
+
+ case 'spot':
+ lightNode = new THREE.SpotLight( color );
+ lightNode.position.set( 0, 0, 1 );
+ break;
+
+ case 'ambient':
+ lightNode = new THREE.AmbientLight( color );
+ break;
+
+ }
+
+ if ( lightNode ) {
+
+ if ( light.constantAttenuation !== undefined ) {
+
+ lightNode.intensity = light.constantAttenuation;
+
+ }
+
+ if ( light.linearAttenuation !== undefined ) {
+
+ lightNode.distance = 1 / light.linearAttenuation;
+
+ }
+
+ if ( light.quadraticAttenuation !== undefined ) {
+
+ lightNode.decay = light.quadraticAttenuation;
+
+ }
+
+ if ( light.fallOffAngle !== undefined ) {
+
+ lightNode.angle = light.fallOffAngle;
+
+ }
+
+ if ( light.fallOffExponent !== undefined ) {
+
+ console.warn( 'THREE.GLTFLoader:: light.fallOffExponent not currently supported.' );
+
+ }
+
+ lightNode.name = light.name || ( 'light_' + lightId );
+ this.lights[ lightId ] = lightNode;
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Common Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_common
+ */
+ function GLTFMaterialsCommonExtension( json ) {
+
+ this.name = EXTENSIONS.KHR_MATERIALS_COMMON;
+
+ }
+
+ GLTFMaterialsCommonExtension.prototype.getMaterialType = function ( material ) {
+
+ var khrMaterial = material.extensions[ this.name ];
+
+ switch ( khrMaterial.type ) {
+
+ case 'commonBlinn' :
+ case 'commonPhong' :
+ return THREE.MeshPhongMaterial;
+
+ case 'commonLambert' :
+ return THREE.MeshLambertMaterial;
+
+ case 'commonConstant' :
+ default :
+ return THREE.MeshBasicMaterial;
+
+ }
+
+ };
+
+ GLTFMaterialsCommonExtension.prototype.extendParams = function ( materialParams, material, parser ) {
+
+ var khrMaterial = material.extensions[ this.name ];
+
+ var pending = [];
+
+ var keys = [];
+
+ // TODO: Currently ignored: 'ambientFactor', 'ambientTexture'
+ switch ( khrMaterial.type ) {
+
+ case 'commonBlinn' :
+ case 'commonPhong' :
+ keys.push( 'diffuseFactor', 'diffuseTexture', 'specularFactor', 'specularTexture', 'shininessFactor' );
+ break;
+
+ case 'commonLambert' :
+ keys.push( 'diffuseFactor', 'diffuseTexture' );
+ break;
+
+ case 'commonConstant' :
+ default :
+ break;
+
+ }
+
+ var materialValues = {};
+
+ keys.forEach( function ( v ) {
+
+ if ( khrMaterial[ v ] !== undefined ) materialValues[ v ] = khrMaterial[ v ];
+
+ } );
+
+ if ( materialValues.diffuseFactor !== undefined ) {
+
+ materialParams.color = new THREE.Color().fromArray( materialValues.diffuseFactor );
+ materialParams.opacity = materialValues.diffuseFactor[ 3 ];
+
+ }
+
+ if ( materialValues.diffuseTexture !== undefined ) {
+
+ pending.push( parser.assignTexture( materialParams, 'map', materialValues.diffuseTexture.index ) );
+
+ }
+
+ if ( materialValues.specularFactor !== undefined ) {
+
+ materialParams.specular = new THREE.Color().fromArray( materialValues.specularFactor );
+
+ }
+
+ if ( materialValues.specularTexture !== undefined ) {
+
+ pending.push( parser.assignTexture( materialParams, 'specularMap', materialValues.specularTexture.index ) );
+
+ }
+
+ if ( materialValues.shininessFactor !== undefined ) {
+
+ materialParams.shininess = materialValues.shininessFactor;
+
+ }
+
+ return Promise.all( pending );
+
+ };
+
+ /* BINARY EXTENSION */
+
+ var BINARY_EXTENSION_BUFFER_NAME = 'binary_glTF';
+ var BINARY_EXTENSION_HEADER_MAGIC = 'glTF';
+ var BINARY_EXTENSION_HEADER_LENGTH = 12;
+ var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };
+
+ function GLTFBinaryExtension( data ) {
+
+ this.name = EXTENSIONS.KHR_BINARY_GLTF;
+ this.content = null;
+ this.body = null;
+
+ var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH );
+
+ this.header = {
+ magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ),
+ version: headerView.getUint32( 4, true ),
+ length: headerView.getUint32( 8, true )
+ };
+
+ if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) {
+
+ throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' );
+
+ } else if ( this.header.version < 2.0 ) {
+
+ throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.' );
+
+ }
+
+ var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH );
+ var chunkIndex = 0;
+
+ while ( chunkIndex < chunkView.byteLength ) {
+
+ var chunkLength = chunkView.getUint32( chunkIndex, true );
+ chunkIndex += 4;
+
+ var chunkType = chunkView.getUint32( chunkIndex, true );
+ chunkIndex += 4;
+
+ if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) {
+
+ var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength );
+ this.content = THREE.LoaderUtils.decodeText( contentArray );
+
+ } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) {
+
+ var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex;
+ this.body = data.slice( byteOffset, byteOffset + chunkLength );
+
+ }
+
+ // Clients must ignore chunks with unknown types.
+
+ chunkIndex += chunkLength;
+
+ }
+
+ if ( this.content === null ) {
+
+ throw new Error( 'THREE.GLTFLoader: JSON content not found.' );
+
+ }
+
+ }
+
+ /**
+ * Specular-Glossiness Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_pbrSpecularGlossiness
+ */
+ function GLTFMaterialsPbrSpecularGlossinessExtension() {
+
+ return {
+
+ name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS,
+
+ specularGlossinessParams: [
+ 'color',
+ 'map',
+ 'lightMap',
+ 'lightMapIntensity',
+ 'aoMap',
+ 'aoMapIntensity',
+ 'emissive',
+ 'emissiveIntensity',
+ 'emissiveMap',
+ 'bumpMap',
+ 'bumpScale',
+ 'normalMap',
+ 'displacementMap',
+ 'displacementScale',
+ 'displacementBias',
+ 'specularMap',
+ 'specular',
+ 'glossinessMap',
+ 'glossiness',
+ 'alphaMap',
+ 'envMap',
+ 'envMapIntensity',
+ 'refractionRatio',
+ ],
+
+ getMaterialType: function () {
+
+ return THREE.ShaderMaterial;
+
+ },
+
+ extendParams: function ( params, material, parser ) {
+
+ var pbrSpecularGlossiness = material.extensions[ this.name ];
+
+ var shader = THREE.ShaderLib[ 'standard' ];
+
+ var uniforms = THREE.UniformsUtils.clone( shader.uniforms );
+
+ var specularMapParsFragmentChunk = [
+ '#ifdef USE_SPECULARMAP',
+ ' uniform sampler2D specularMap;',
+ '#endif'
+ ].join( '\n' );
+
+ var glossinessMapParsFragmentChunk = [
+ '#ifdef USE_GLOSSINESSMAP',
+ ' uniform sampler2D glossinessMap;',
+ '#endif'
+ ].join( '\n' );
+
+ var specularMapFragmentChunk = [
+ 'vec3 specularFactor = specular;',
+ '#ifdef USE_SPECULARMAP',
+ ' vec4 texelSpecular = texture2D( specularMap, vUv );',
+ ' texelSpecular = sRGBToLinear( texelSpecular );',
+ ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture',
+ ' specularFactor *= texelSpecular.rgb;',
+ '#endif'
+ ].join( '\n' );
+
+ var glossinessMapFragmentChunk = [
+ 'float glossinessFactor = glossiness;',
+ '#ifdef USE_GLOSSINESSMAP',
+ ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );',
+ ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture',
+ ' glossinessFactor *= texelGlossiness.a;',
+ '#endif'
+ ].join( '\n' );
+
+ var lightPhysicalFragmentChunk = [
+ 'PhysicalMaterial material;',
+ 'material.diffuseColor = diffuseColor.rgb;',
+ 'material.specularRoughness = clamp( 1.0 - glossinessFactor, 0.04, 1.0 );',
+ 'material.specularColor = specularFactor.rgb;',
+ ].join( '\n' );
+
+ var fragmentShader = shader.fragmentShader
+ .replace( '#include ', '' )
+ .replace( 'uniform float roughness;', 'uniform vec3 specular;' )
+ .replace( 'uniform float metalness;', 'uniform float glossiness;' )
+ .replace( '#include ', specularMapParsFragmentChunk )
+ .replace( '#include ', glossinessMapParsFragmentChunk )
+ .replace( '#include ', specularMapFragmentChunk )
+ .replace( '#include ', glossinessMapFragmentChunk )
+ .replace( '#include ', lightPhysicalFragmentChunk );
+
+ delete uniforms.roughness;
+ delete uniforms.metalness;
+ delete uniforms.roughnessMap;
+ delete uniforms.metalnessMap;
+
+ uniforms.specular = { value: new THREE.Color().setHex( 0x111111 ) };
+ uniforms.glossiness = { value: 0.5 };
+ uniforms.specularMap = { value: null };
+ uniforms.glossinessMap = { value: null };
+
+ params.vertexShader = shader.vertexShader;
+ params.fragmentShader = fragmentShader;
+ params.uniforms = uniforms;
+ params.defines = { 'STANDARD': '' };
+
+ params.color = new THREE.Color( 1.0, 1.0, 1.0 );
+ params.opacity = 1.0;
+
+ var pending = [];
+
+ if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) {
+
+ var array = pbrSpecularGlossiness.diffuseFactor;
+
+ params.color.fromArray( array );
+ params.opacity = array[ 3 ];
+
+ }
+
+ if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) {
+
+ pending.push( parser.assignTexture( params, 'map', pbrSpecularGlossiness.diffuseTexture.index ) );
+
+ }
+
+ params.emissive = new THREE.Color( 0.0, 0.0, 0.0 );
+ params.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0;
+ params.specular = new THREE.Color( 1.0, 1.0, 1.0 );
+
+ if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) {
+
+ params.specular.fromArray( pbrSpecularGlossiness.specularFactor );
+
+ }
+
+ if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) {
+
+ var specGlossIndex = pbrSpecularGlossiness.specularGlossinessTexture.index;
+ pending.push( parser.assignTexture( params, 'glossinessMap', specGlossIndex ) );
+ pending.push( parser.assignTexture( params, 'specularMap', specGlossIndex ) );
+
+ }
+
+ return Promise.all( pending );
+
+ },
+
+ createMaterial: function ( params ) {
+
+ // setup material properties based on MeshStandardMaterial for Specular-Glossiness
+
+ var material = new THREE.ShaderMaterial( {
+ defines: params.defines,
+ vertexShader: params.vertexShader,
+ fragmentShader: params.fragmentShader,
+ uniforms: params.uniforms,
+ fog: true,
+ lights: true,
+ opacity: params.opacity,
+ transparent: params.transparent
+ } );
+
+ material.isGLTFSpecularGlossinessMaterial = true;
+
+ material.color = params.color;
+
+ material.map = params.map === undefined ? null : params.map;
+
+ material.lightMap = null;
+ material.lightMapIntensity = 1.0;
+
+ material.aoMap = params.aoMap === undefined ? null : params.aoMap;
+ material.aoMapIntensity = 1.0;
+
+ material.emissive = params.emissive;
+ material.emissiveIntensity = 1.0;
+ material.emissiveMap = params.emissiveMap === undefined ? null : params.emissiveMap;
+
+ material.bumpMap = params.bumpMap === undefined ? null : params.bumpMap;
+ material.bumpScale = 1;
+
+ material.normalMap = params.normalMap === undefined ? null : params.normalMap;
+ if ( params.normalScale ) material.normalScale = params.normalScale;
+
+ material.displacementMap = null;
+ material.displacementScale = 1;
+ material.displacementBias = 0;
+
+ material.specularMap = params.specularMap === undefined ? null : params.specularMap;
+ material.specular = params.specular;
+
+ material.glossinessMap = params.glossinessMap === undefined ? null : params.glossinessMap;
+ material.glossiness = params.glossiness;
+
+ material.alphaMap = null;
+
+ material.envMap = params.envMap === undefined ? null : params.envMap;
+ material.envMapIntensity = 1.0;
+
+ material.refractionRatio = 0.98;
+
+ material.extensions.derivatives = true;
+
+ return material;
+
+ },
+
+ /**
+ * Clones a GLTFSpecularGlossinessMaterial instance. The ShaderMaterial.copy() method can
+ * copy only properties it knows about or inherits, and misses many properties that would
+ * normally be defined by MeshStandardMaterial.
+ *
+ * This method allows GLTFSpecularGlossinessMaterials to be cloned in the process of
+ * loading a glTF model, but cloning later (e.g. by the user) would require these changes
+ * AND also updating `.onBeforeRender` on the parent mesh.
+ *
+ * @param {THREE.ShaderMaterial} source
+ * @return {THREE.ShaderMaterial}
+ */
+ cloneMaterial: function ( source ) {
+
+ var target = source.clone();
+
+ target.isGLTFSpecularGlossinessMaterial = true;
+
+ var params = this.specularGlossinessParams;
+
+ for ( var i = 0, il = params.length; i < il; i ++ ) {
+
+ target[ params[ i ] ] = source[ params[ i ] ];
+
+ }
+
+ return target;
+
+ },
+
+ // Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer.
+ refreshUniforms: function ( renderer, scene, camera, geometry, material, group ) {
+
+ var uniforms = material.uniforms;
+ var defines = material.defines;
+
+ uniforms.opacity.value = material.opacity;
+
+ uniforms.diffuse.value.copy( material.color );
+ uniforms.emissive.value.copy( material.emissive ).multiplyScalar( material.emissiveIntensity );
+
+ uniforms.map.value = material.map;
+ uniforms.specularMap.value = material.specularMap;
+ uniforms.alphaMap.value = material.alphaMap;
+
+ uniforms.lightMap.value = material.lightMap;
+ uniforms.lightMapIntensity.value = material.lightMapIntensity;
+
+ uniforms.aoMap.value = material.aoMap;
+ uniforms.aoMapIntensity.value = material.aoMapIntensity;
+
+ // uv repeat and offset setting priorities
+ // 1. color map
+ // 2. specular map
+ // 3. normal map
+ // 4. bump map
+ // 5. alpha map
+ // 6. emissive map
+
+ var uvScaleMap;
+
+ if ( material.map ) {
+
+ uvScaleMap = material.map;
+
+ } else if ( material.specularMap ) {
+
+ uvScaleMap = material.specularMap;
+
+ } else if ( material.displacementMap ) {
+
+ uvScaleMap = material.displacementMap;
+
+ } else if ( material.normalMap ) {
+
+ uvScaleMap = material.normalMap;
+
+ } else if ( material.bumpMap ) {
+
+ uvScaleMap = material.bumpMap;
+
+ } else if ( material.glossinessMap ) {
+
+ uvScaleMap = material.glossinessMap;
+
+ } else if ( material.alphaMap ) {
+
+ uvScaleMap = material.alphaMap;
+
+ } else if ( material.emissiveMap ) {
+
+ uvScaleMap = material.emissiveMap;
+
+ }
+
+ if ( uvScaleMap !== undefined ) {
+
+ // backwards compatibility
+ if ( uvScaleMap.isWebGLRenderTarget ) {
+
+ uvScaleMap = uvScaleMap.texture;
+
+ }
+
+ var offset;
+ var repeat;
+
+ if ( uvScaleMap.matrix !== undefined ) {
+
+ // > r88.
+
+ if ( uvScaleMap.matrixAutoUpdate === true ) {
+
+ offset = uvScaleMap.offset;
+ repeat = uvScaleMap.repeat;
+ var rotation = uvScaleMap.rotation;
+ var center = uvScaleMap.center;
+
+ uvScaleMap.matrix.setUvTransform( offset.x, offset.y, repeat.x, repeat.y, rotation, center.x, center.y );
+
+ }
+
+ uniforms.uvTransform.value.copy( uvScaleMap.matrix );
+
+ } else {
+
+ // <= r87. Remove when reasonable.
+
+ offset = uvScaleMap.offset;
+ repeat = uvScaleMap.repeat;
+
+ uniforms.offsetRepeat.value.set( offset.x, offset.y, repeat.x, repeat.y );
+
+ }
+
+ }
+
+ uniforms.envMap.value = material.envMap;
+ uniforms.envMapIntensity.value = material.envMapIntensity;
+ uniforms.flipEnvMap.value = ( material.envMap && material.envMap.isCubeTexture ) ? - 1 : 1;
+
+ uniforms.refractionRatio.value = material.refractionRatio;
+
+ uniforms.specular.value.copy( material.specular );
+ uniforms.glossiness.value = material.glossiness;
+
+ uniforms.glossinessMap.value = material.glossinessMap;
+
+ uniforms.emissiveMap.value = material.emissiveMap;
+ uniforms.bumpMap.value = material.bumpMap;
+ uniforms.normalMap.value = material.normalMap;
+
+ uniforms.displacementMap.value = material.displacementMap;
+ uniforms.displacementScale.value = material.displacementScale;
+ uniforms.displacementBias.value = material.displacementBias;
+
+ if ( uniforms.glossinessMap.value !== null && defines.USE_GLOSSINESSMAP === undefined ) {
+
+ defines.USE_GLOSSINESSMAP = '';
+ // set USE_ROUGHNESSMAP to enable vUv
+ defines.USE_ROUGHNESSMAP = '';
+
+ }
+
+ if ( uniforms.glossinessMap.value === null && defines.USE_GLOSSINESSMAP !== undefined ) {
+
+ delete defines.USE_GLOSSINESSMAP;
+ delete defines.USE_ROUGHNESSMAP;
+
+ }
+
+ }
+
+ };
+
+ }
+
+ /*********************************/
+ /********** INTERNALS ************/
+ /*********************************/
+
+ /* CONSTANTS */
+
+ var WEBGL_CONSTANTS = {
+ FLOAT: 5126,
+ //FLOAT_MAT2: 35674,
+ FLOAT_MAT3: 35675,
+ FLOAT_MAT4: 35676,
+ FLOAT_VEC2: 35664,
+ FLOAT_VEC3: 35665,
+ FLOAT_VEC4: 35666,
+ LINEAR: 9729,
+ REPEAT: 10497,
+ SAMPLER_2D: 35678,
+ POINTS: 0,
+ LINES: 1,
+ LINE_LOOP: 2,
+ LINE_STRIP: 3,
+ TRIANGLES: 4,
+ TRIANGLE_STRIP: 5,
+ TRIANGLE_FAN: 6,
+ UNSIGNED_BYTE: 5121,
+ UNSIGNED_SHORT: 5123
+ };
+
+ var WEBGL_TYPE = {
+ 5126: Number,
+ //35674: THREE.Matrix2,
+ 35675: THREE.Matrix3,
+ 35676: THREE.Matrix4,
+ 35664: THREE.Vector2,
+ 35665: THREE.Vector3,
+ 35666: THREE.Vector4,
+ 35678: THREE.Texture
+ };
+
+ var WEBGL_COMPONENT_TYPES = {
+ 5120: Int8Array,
+ 5121: Uint8Array,
+ 5122: Int16Array,
+ 5123: Uint16Array,
+ 5125: Uint32Array,
+ 5126: Float32Array
+ };
+
+ var WEBGL_FILTERS = {
+ 9728: THREE.NearestFilter,
+ 9729: THREE.LinearFilter,
+ 9984: THREE.NearestMipMapNearestFilter,
+ 9985: THREE.LinearMipMapNearestFilter,
+ 9986: THREE.NearestMipMapLinearFilter,
+ 9987: THREE.LinearMipMapLinearFilter
+ };
+
+ var WEBGL_WRAPPINGS = {
+ 33071: THREE.ClampToEdgeWrapping,
+ 33648: THREE.MirroredRepeatWrapping,
+ 10497: THREE.RepeatWrapping
+ };
+
+ var WEBGL_TEXTURE_FORMATS = {
+ 6406: THREE.AlphaFormat,
+ 6407: THREE.RGBFormat,
+ 6408: THREE.RGBAFormat,
+ 6409: THREE.LuminanceFormat,
+ 6410: THREE.LuminanceAlphaFormat
+ };
+
+ var WEBGL_TEXTURE_DATATYPES = {
+ 5121: THREE.UnsignedByteType,
+ 32819: THREE.UnsignedShort4444Type,
+ 32820: THREE.UnsignedShort5551Type,
+ 33635: THREE.UnsignedShort565Type
+ };
+
+ var WEBGL_SIDES = {
+ 1028: THREE.BackSide, // Culling front
+ 1029: THREE.FrontSide // Culling back
+ //1032: THREE.NoSide // Culling front and back, what to do?
+ };
+
+ var WEBGL_DEPTH_FUNCS = {
+ 512: THREE.NeverDepth,
+ 513: THREE.LessDepth,
+ 514: THREE.EqualDepth,
+ 515: THREE.LessEqualDepth,
+ 516: THREE.GreaterEqualDepth,
+ 517: THREE.NotEqualDepth,
+ 518: THREE.GreaterEqualDepth,
+ 519: THREE.AlwaysDepth
+ };
+
+ var WEBGL_BLEND_EQUATIONS = {
+ 32774: THREE.AddEquation,
+ 32778: THREE.SubtractEquation,
+ 32779: THREE.ReverseSubtractEquation
+ };
+
+ var WEBGL_BLEND_FUNCS = {
+ 0: THREE.ZeroFactor,
+ 1: THREE.OneFactor,
+ 768: THREE.SrcColorFactor,
+ 769: THREE.OneMinusSrcColorFactor,
+ 770: THREE.SrcAlphaFactor,
+ 771: THREE.OneMinusSrcAlphaFactor,
+ 772: THREE.DstAlphaFactor,
+ 773: THREE.OneMinusDstAlphaFactor,
+ 774: THREE.DstColorFactor,
+ 775: THREE.OneMinusDstColorFactor,
+ 776: THREE.SrcAlphaSaturateFactor
+ // The followings are not supported by Three.js yet
+ //32769: CONSTANT_COLOR,
+ //32770: ONE_MINUS_CONSTANT_COLOR,
+ //32771: CONSTANT_ALPHA,
+ //32772: ONE_MINUS_CONSTANT_COLOR
+ };
+
+ var WEBGL_TYPE_SIZES = {
+ 'SCALAR': 1,
+ 'VEC2': 2,
+ 'VEC3': 3,
+ 'VEC4': 4,
+ 'MAT2': 4,
+ 'MAT3': 9,
+ 'MAT4': 16
+ };
+
+ var PATH_PROPERTIES = {
+ scale: 'scale',
+ translation: 'position',
+ rotation: 'quaternion',
+ weights: 'morphTargetInfluences'
+ };
+
+ var INTERPOLATION = {
+ CUBICSPLINE: THREE.InterpolateSmooth,
+ LINEAR: THREE.InterpolateLinear,
+ STEP: THREE.InterpolateDiscrete
+ };
+
+ var STATES_ENABLES = {
+ 2884: 'CULL_FACE',
+ 2929: 'DEPTH_TEST',
+ 3042: 'BLEND',
+ 3089: 'SCISSOR_TEST',
+ 32823: 'POLYGON_OFFSET_FILL',
+ 32926: 'SAMPLE_ALPHA_TO_COVERAGE'
+ };
+
+ var ALPHA_MODES = {
+ OPAQUE: 'OPAQUE',
+ MASK: 'MASK',
+ BLEND: 'BLEND'
+ };
+
+ /* UTILITY FUNCTIONS */
+
+ function resolveURL( url, path ) {
+
+ // Invalid URL
+ if ( typeof url !== 'string' || url === '' ) return '';
+
+ // Absolute URL http://,https://,//
+ if ( /^(https?:)?\/\//i.test( url ) ) return url;
+
+ // Data URI
+ if ( /^data:.*,.*$/i.test( url ) ) return url;
+
+ // Blob URL
+ if ( /^blob:.*$/i.test( url ) ) return url;
+
+ // Relative URL
+ return path + url;
+
+ }
+
+ /**
+ * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
+ */
+ function createDefaultMaterial() {
+
+ return new THREE.MeshStandardMaterial( {
+ color: 0xFFFFFF,
+ emissive: 0x000000,
+ metalness: 1,
+ roughness: 1,
+ transparent: false,
+ depthTest: true,
+ side: THREE.FrontSide
+ } );
+
+ }
+
+ /**
+ * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
+ *
+ * TODO: Implement support for morph targets on TANGENT attribute.
+ *
+ * @param {THREE.Mesh} mesh
+ * @param {GLTF.Mesh} meshDef
+ * @param {GLTF.Primitive} primitiveDef
+ * @param {Array} accessors
+ */
+ function addMorphTargets( mesh, meshDef, primitiveDef, accessors ) {
+
+ var geometry = mesh.geometry;
+ var material = mesh.material;
+
+ var targets = primitiveDef.targets;
+ var morphAttributes = geometry.morphAttributes;
+
+ morphAttributes.position = [];
+ morphAttributes.normal = [];
+
+ material.morphTargets = true;
+
+ for ( var i = 0, il = targets.length; i < il; i ++ ) {
+
+ var target = targets[ i ];
+ var attributeName = 'morphTarget' + i;
+
+ var positionAttribute, normalAttribute;
+
+ if ( target.POSITION !== undefined ) {
+
+ // Three.js morph formula is
+ // position
+ // + weight0 * ( morphTarget0 - position )
+ // + weight1 * ( morphTarget1 - position )
+ // ...
+ // while the glTF one is
+ // position
+ // + weight0 * morphTarget0
+ // + weight1 * morphTarget1
+ // ...
+ // then adding position to morphTarget.
+ // So morphTarget value will depend on mesh's position, then cloning attribute
+ // for the case if attribute is shared among two or more meshes.
+
+ positionAttribute = accessors[ target.POSITION ].clone();
+ var position = geometry.attributes.position;
+
+ for ( var j = 0, jl = positionAttribute.count; j < jl; j ++ ) {
+
+ positionAttribute.setXYZ(
+ j,
+ positionAttribute.getX( j ) + position.getX( j ),
+ positionAttribute.getY( j ) + position.getY( j ),
+ positionAttribute.getZ( j ) + position.getZ( j )
+ );
+
+ }
+
+ } else if ( geometry.attributes.position ) {
+
+ // Copying the original position not to affect the final position.
+ // See the formula above.
+ positionAttribute = geometry.attributes.position.clone();
+
+ }
+
+ if ( positionAttribute !== undefined ) {
+
+ positionAttribute.name = attributeName;
+ morphAttributes.position.push( positionAttribute );
+
+ }
+
+ if ( target.NORMAL !== undefined ) {
+
+ material.morphNormals = true;
+
+ // see target.POSITION's comment
+
+ normalAttribute = accessors[ target.NORMAL ].clone();
+ var normal = geometry.attributes.normal;
+
+ for ( var j = 0, jl = normalAttribute.count; j < jl; j ++ ) {
+
+ normalAttribute.setXYZ(
+ j,
+ normalAttribute.getX( j ) + normal.getX( j ),
+ normalAttribute.getY( j ) + normal.getY( j ),
+ normalAttribute.getZ( j ) + normal.getZ( j )
+ );
+
+ }
+
+ } else if ( geometry.attributes.normal !== undefined ) {
+
+ normalAttribute = geometry.attributes.normal.clone();
+
+ }
+
+ if ( normalAttribute !== undefined ) {
+
+ normalAttribute.name = attributeName;
+ morphAttributes.normal.push( normalAttribute );
+
+ }
+
+ }
+
+ mesh.updateMorphTargets();
+
+ if ( meshDef.weights !== undefined ) {
+
+ for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) {
+
+ mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ];
+
+ }
+
+ }
+
+ }
+
+ function isPrimitiveEqual( a, b ) {
+
+ if ( a.indices !== b.indices ) {
+
+ return false;
+
+ }
+
+ var attribA = a.attributes || {};
+ var attribB = b.attributes || {};
+ var keysA = Object.keys( attribA );
+ var keysB = Object.keys( attribB );
+
+ if ( keysA.length !== keysB.length ) {
+
+ return false;
+
+ }
+
+ for ( var i = 0, il = keysA.length; i < il; i ++ ) {
+
+ var key = keysA[ i ];
+
+ if ( attribA[ key ] !== attribB[ key ] ) {
+
+ return false;
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+ function getCachedGeometry( cache, newPrimitive ) {
+
+ for ( var i = 0, il = cache.length; i < il; i ++ ) {
+
+ var cached = cache[ i ];
+
+ if ( isPrimitiveEqual( cached.primitive, newPrimitive ) ) {
+
+ return cached.geometry;
+
+ }
+
+ }
+
+ return null;
+
+ }
+
+ /* GLTF PARSER */
+
+ function GLTFParser( json, extensions, options ) {
+
+ this.json = json || {};
+ this.extensions = extensions || {};
+ this.options = options || {};
+
+ // loader object cache
+ this.cache = new GLTFRegistry();
+
+ // BufferGeometry caching
+ this.primitiveCache = [];
+
+ this.textureLoader = new THREE.TextureLoader( this.options.manager );
+ this.textureLoader.setCrossOrigin( this.options.crossOrigin );
+
+ this.fileLoader = new THREE.FileLoader( this.options.manager );
+ this.fileLoader.setResponseType( 'arraybuffer' );
+
+ }
+
+ GLTFParser.prototype.parse = function ( onLoad, onError ) {
+
+ var json = this.json;
+ var parser = this;
+
+ // Clear the loader cache
+ this.cache.removeAll();
+
+ // Mark the special nodes/meshes in json for efficient parse
+ this.markDefs();
+
+ // Fire the callback on complete
+ this.getMultiDependencies( [
+
+ 'scene',
+ 'animation',
+ 'camera'
+
+ ] ).then( function ( dependencies ) {
+
+ var scenes = dependencies.scenes || [];
+ var scene = scenes[ json.scene || 0 ];
+ var animations = dependencies.animations || [];
+ var asset = json.asset;
+ var cameras = dependencies.cameras || [];
+
+ onLoad( scene, scenes, cameras, animations, asset );
+
+ } ).catch( onError );
+
+ };
+
+ /**
+ * Marks the special nodes/meshes in json for efficient parse.
+ */
+ GLTFParser.prototype.markDefs = function () {
+
+ var nodeDefs = this.json.nodes || [];
+ var skinDefs = this.json.skins || [];
+ var meshDefs = this.json.meshes || [];
+
+ var meshReferences = {};
+ var meshUses = {};
+
+ // Nothing in the node definition indicates whether it is a Bone or an
+ // Object3D. Use the skins' joint references to mark bones.
+ for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) {
+
+ var joints = skinDefs[ skinIndex ].joints;
+
+ for ( var i = 0, il = joints.length; i < il; i ++ ) {
+
+ nodeDefs[ joints[ i ] ].isBone = true;
+
+ }
+
+ }
+
+ // Meshes can (and should) be reused by multiple nodes in a glTF asset. To
+ // avoid having more than one THREE.Mesh with the same name, count
+ // references and rename instances below.
+ //
+ // Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
+ for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) {
+
+ var nodeDef = nodeDefs[ nodeIndex ];
+
+ if ( nodeDef.mesh !== undefined ) {
+
+ if ( meshReferences[ nodeDef.mesh ] === undefined ) {
+
+ meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0;
+
+ }
+
+ meshReferences[ nodeDef.mesh ] ++;
+
+ // Nothing in the mesh definition indicates whether it is
+ // a SkinnedMesh or Mesh. Use the node's mesh reference
+ // to mark SkinnedMesh if node has skin.
+ if ( nodeDef.skin !== undefined ) {
+
+ meshDefs[ nodeDef.mesh ].isSkinnedMesh = true;
+
+ }
+
+ }
+
+ }
+
+ this.json.meshReferences = meshReferences;
+ this.json.meshUses = meshUses;
+
+ };
+
+ /**
+ * Requests the specified dependency asynchronously, with caching.
+ * @param {string} type
+ * @param {number} index
+ * @return {Promise