filestash/public/assets/pages/viewerpage/application_3d/init.js
MickaelK 71b14e6eaf feature (3d): embed 3d viewer anywhere
This contains a bunch of things packaged in 1:

1) UI improvements for the 3D viewer to support all sort of file types
   and create a nice rendering in a clean way with all sort of options

2) enable people to use Filestash as an SDK so we can embed the 3d viewer
   elsewhere
2024-12-23 18:50:23 +11:00

137 lines
8.9 KiB
JavaScript

import { createElement, onDestroy } from "../../../lib/skeleton/index.js";
import { join } from "../../../lib/path.js";
import * as THREE from "../../../lib/vendor/three/three.module.js";
import { OrbitControls } from "../../../lib/vendor/three/OrbitControls.js";
import { toCreasedNormals } from "../../../lib/vendor/three/utils/BufferGeometryUtils.js";
import { GLTFLoader } from "../../../lib/vendor/three/GLTFLoader.js";
import { OBJLoader } from "../../../lib/vendor/three/OBJLoader.js";
import { STLLoader } from "../../../lib/vendor/three/STLLoader.js";
import { FBXLoader } from "../../../lib/vendor/three/FBXLoader.js";
import { Rhino3dmLoader } from "../../../lib/vendor/three/3DMLoader.js";
export default function({ $page, $menubar, mesh, refresh }) {
// setup the dom
const renderer = new THREE.WebGLRenderer({ antialias: true, shadowMapEnabled: true });
renderer.setSize($page.clientWidth, $page.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0xf5f5f5);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
$page.appendChild(renderer.domElement);
// center everything
const box = new THREE.Box3().setFromObject(mesh);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
// setup the scene, camera and controls
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
$page.clientWidth / $page.clientHeight,
Math.max(0.1, maxDim / 100),
maxDim * 10,
);
const controls = new OrbitControls(camera, renderer.domElement);
scene.add(mesh);
mesh.castShadow = true;
mesh.receiveShadow = true;
camera.position.set(center.x, center.y, center.z + maxDim * 1.8);
controls.target.copy(center);
// enable animation if present
const mixer = new THREE.AnimationMixer(mesh);
if (mesh.animations.length > 0) {
const ICON = {
PLAY: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmlld0JveD0iMCAwIDU4Ljc1MiA1OC43NTIiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzE1OCIKICAgc29kaXBvZGk6ZG9jbmFtZT0icGxheS5zdmciCiAgIGlua3NjYXBlOnZlcnNpb249IjEuMi4yIChiMGE4NDg2NTQxLCAyMDIyLTEyLTAxKSIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzMTYyIiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0ibmFtZWR2aWV3MTYwIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzAwMDAwMCIKICAgICBib3JkZXJvcGFjaXR5PSIwLjI1IgogICAgIGlua3NjYXBlOnNob3dwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIKICAgICBpbmtzY2FwZTpkZXNrY29sb3I9IiNkMWQxZDEiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGlua3NjYXBlOnpvb209IjE1LjQyMDc1MiIKICAgICBpbmtzY2FwZTpjeD0iMjkuMzQzNTc2IgogICAgIGlua3NjYXBlOmN5PSIyNS45MzkwNzMiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxOTA0IgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjExNTciCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjciCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjM0IgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnMTU4IiAvPgogIDxwYXRoCiAgICAgZD0iTSA0NS42MDY5MTMsMjUuMTc0NzEyIDIyLjY5MTQwMSw2LjEwODk4MjUgYyAtMS40Njk2MjUsLTAuOTA3ODUyNSAtMy4zNzIzNTQsLTAuOTA1Mzc2NiAtNC44MzY1ODQsMCAtMS40OTM1MTUsMC45MjEwNTc3IC0yLjQyMjE0NSwyLjY0MTg1MSAtMi40MjIxNDUsNC40ODk3NDM1IFYgNDguNzMyNjYgYyAwLDEuODQ4NzE4IDAuOTI3ODYsMy41Njk1MTEgMi40MTI4OTcsNC40ODU2MTcgMC43MzQ0MjcsMC40NTgwNTMgMS41NzM2NjMsMC42OTk4NzIgMi40MjY3NjksMC42OTk4NzIgMC44NTA3OTUsMCAxLjY4OTI2LC0wLjI0MDk5NCAyLjQyMDYwNCwtMC42OTU3NDUgbCAyMi45MTU1MTIsLTE5LjA2NzM4IGMgMS40OTE5NzQsLTAuOTIzNTMzIDIuNDE4MjkyLC0yLjY0MzUwMSAyLjQxODI5MiwtNC40ODg5MTggLTcuN2UtNCwtMS44NDI5NDEgLTAuOTI2MzE4LC0zLjU2MjkwOSAtMi40MTk4MzMsLTQuNDkxMzk0IHogbSAtMi42MDU3MDIsNC42OTM1OTggYyAtMjguNjY3NDc0LC0xOS45MTIyMDY3IC0xNC4zMzM3MzcsLTkuOTU2MTAzIDAsMCB6IgogICAgIHN0eWxlPSJmaWxsOiNmMmYyZjIiCiAgICAgaWQ9InBhdGgxNTYiCiAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc2NzY2NjY2NjIiAvPgo8L3N2Zz4K",
PAUSE: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmlld0JveD0iMCAwIDU4Ljc1MiA1OC43NTIiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzE1OCIKICAgc29kaXBvZGk6ZG9jbmFtZT0icGF1c2Uuc3ZnIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjIuMiAoYjBhODQ4NjU0MSwgMjAyMi0xMi0wMSkiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnMKICAgICBpZD0iZGVmczE2MiIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzE2MCIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiMwMDAwMDAiCiAgICAgYm9yZGVyb3BhY2l0eT0iMC4yNSIKICAgICBpbmtzY2FwZTpzaG93cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgaW5rc2NhcGU6ZGVza2NvbG9yPSIjZDFkMWQxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp6b29tPSIxNS40MjA3NTIiCiAgICAgaW5rc2NhcGU6Y3g9IjI4LjQzNTcwOSIKICAgICBpbmtzY2FwZTpjeT0iMjUuOTM5MDczIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkwNCIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMTU3IgogICAgIGlua3NjYXBlOndpbmRvdy14PSI3IgogICAgIGlua3NjYXBlOndpbmRvdy15PSIzNCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2ZzE1OCIgLz4KICA8cmVjdAogICAgIHN0eWxlPSJmaWxsOiNmMmYyZjI7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjAuOTM2NjQxIgogICAgIGlkPSJyZWN0NTA3IgogICAgIHdpZHRoPSIzOS42NTU5MTQiCiAgICAgaGVpZ2h0PSI0NS4xMjEzNTciCiAgICAgeD0iMTAuMzExNDcyIgogICAgIHk9IjcuMTUyMjY4OSIKICAgICByeD0iNS43NSIgLz4KPC9zdmc+Cg==",
};
const $button = createElement(`<img class="component_icon" draggable="false" src="${ICON.PLAY}" alt="play">`);
const action = mixer.clipAction(mesh.animations[0]);
let isPlaying = false;
$button.onclick = () => {
if (isPlaying === false) action.play();
else action.stop();
isPlaying = !isPlaying;
$button.setAttribute("src", isPlaying ? ICON.PAUSE : ICON.PLAY);
};
$menubar.add($button);
}
// sizing of the window
const onResize = () => {
camera.aspect = $page.clientWidth / $page.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize($page.clientWidth, $page.clientHeight);
};
window.addEventListener("resize", onResize);
onDestroy(() => window.removeEventListener("resize", onResize));
// stuff we refresh constantly
const clock = new THREE.Clock();
refresh.push(() => {
controls.update();
renderer.render(scene, camera);
mixer.update(clock.getDelta());
});
return { renderer, scene, camera, controls, box };
}
export function getLoader(mime) {
const identity = (s) => s;
switch (mime) {
case "application/object":
return [
new OBJLoader(),
(obj) => {
obj.traverse((child) => {
if (child.isMesh) {
child.material = new THREE.MeshPhongMaterial({
color: 0x40464b,
emissive: 0x40464b,
specular: 0xf9f9fa,
shininess: 10,
transparent: true,
});
// smooth the edges: https://discourse.threejs.org/t/how-to-smooth-an-obj-with-threejs/3950/16
child.geometry = toCreasedNormals(child.geometry, (30 / 180) * Math.PI);
}
});
return obj;
},
];
case "model/3dm":
THREE.Object3D.DEFAULT_UP.set(0, 0, 1);
const loader = new Rhino3dmLoader();
loader.setLibraryPath(join(import.meta.url, "../../../lib/vendor/three/rhino3dm/"));
return [loader, identity];
case "model/gtlt-binary":
case "model/gltf+json":
return [new GLTFLoader(), (gltf) => gltf.scene];
case "model/stl":
return [new STLLoader(), (geometry) => {
const material = new THREE.MeshPhongMaterial({
emissive: 0x40464b,
specular: 0xf9f9fa,
shininess: 15,
transparent: true,
});
if (geometry.hasColors) material.vertexColors = true;
else material.color = material.emissive;
return new THREE.Mesh(geometry, material);
}];
case "application/fbx":
return [new FBXLoader(), (obj) => {
obj.traverse((child) => {
// console.log(child);
});
return obj;
}];
default:
return [null, null];
}
}