diff --git a/public/assets/components/sidebar.js b/public/assets/components/sidebar.js
index 024a80d8..70b7fd70 100644
--- a/public/assets/components/sidebar.js
+++ b/public/assets/components/sidebar.js
@@ -102,8 +102,8 @@ async function ctrlNavigationPane(render, { $sidebar, nRestart }) {
$anchor.appendChild($list);
} catch (err) {
await cache().remove("/", false);
- if (nRestart < 2) ctrlSidebar(render, nRestart + 1);
- else if (err instanceof DOMException) {}
+ if (err instanceof DOMException) return;
+ else if (nRestart < 2) ctrlSidebar(render, nRestart + 1);
else throw err;
}
}
diff --git a/public/assets/helpers/sdk.js b/public/assets/helpers/sdk.js
new file mode 100644
index 00000000..4754f4b7
--- /dev/null
+++ b/public/assets/helpers/sdk.js
@@ -0,0 +1,15 @@
+// feature detection if we're using Filestash as a standalone app or as an SDK
+// see: ../index.js
+
+export function isSDK() {
+ const importURL = new URL(import.meta.url);
+ return location.origin !== importURL.origin;
+}
+
+export function urlSDK(url) {
+ const importURL = new URL(import.meta.url);
+ if (new RegExp("^/").test(url) === false) {
+ url = "/" + url
+ }
+ return importURL.origin + url;
+}
diff --git a/public/assets/index.js b/public/assets/index.js
new file mode 100644
index 00000000..20b90f79
--- /dev/null
+++ b/public/assets/index.js
@@ -0,0 +1,54 @@
+// Want to integrate Filestash as a SDK in your application? You are in the right place!
+//
+// How it works you may ask? it's simple:
+// 1) pick a component to render. Filestash components look like this:
+// function(render, opts = {}) {
+// // ...
+// }
+// 2) similarly to every framework out there, call the framework bootstrap procedure:
+// render(Component, $node, args = {});
+//
+//
+// /***********************************************/
+// /* example to render the 3D viewer application */
+// /***********************************************/
+// import { render } from "https://demo.filestash.app/assets/index.js";
+// import * as Component from "https://demo.filestash.app/assets/pages/viewerpage/application_3d.js";
+//
+// render(Component, document.getElementById("app"), {});
+//
+//
+//
+import { createRender, createElement } from "./lib/skeleton/index.js";
+import { loadCSS } from "./helpers/loader.js";
+
+export function render(module, $app, opts = {}) {
+ assertArgs(module, $app);
+ execute(module, $app, opts);
+}
+
+function assertArgs(module, $app) {
+ if (typeof module.default !== "function") throw new TypeError("Unsupported module - see documentation on how to use Filestash");
+ else if (!($app instanceof Node)) throw new TypeError("Invalid node - see documentation on how to use Filestash");
+}
+
+function execute(module, $app, opts) {
+ const priors = [
+ import("./boot/ctrl_boot_frontoffice.js"),
+ loadCSS(import.meta.url, "./css/designsystem.css"),
+ ];
+ if (typeof module.init === "function") priors.push(module.init($app));
+
+ return Promise.all(priors)
+ .then(async() => await module.default(createRender($app), opts))
+ .then(() => $app.appendChild(poweredBy()))
+ .catch((err) => console.error(err));
+}
+
+function poweredBy($app) {
+ return createElement(`
+
-
+
${t("home")}
diff --git a/public/assets/pages/ctrl_viewerpage.js b/public/assets/pages/ctrl_viewerpage.js
index bbf03024..68fee5ce 100644
--- a/public/assets/pages/ctrl_viewerpage.js
+++ b/public/assets/pages/ctrl_viewerpage.js
@@ -9,8 +9,8 @@ import { init as initMenubar } from "./viewerpage/component_menubar.js";
import { init as initCache } from "./filespage/cache.js";
import ctrlError from "./ctrl_error.js";
+import { getFilename, getDownloadUrl, getCurrentPath } from "./viewerpage/common.js";
import { opener } from "./viewerpage/mimetype.js";
-import { getCurrentPath } from "./viewerpage/common.js";
import { options } from "./viewerpage/model_files.js";
import "../components/breadcrumb.js";
@@ -54,7 +54,7 @@ export default WithShell(async function(render) {
effect(rxjs.of(window.CONFIG["mime"] || {}).pipe(
rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)),
rxjs.mergeMap(([opener, opts]) => rxjs.from(loadModule(opener)).pipe(rxjs.tap((module) => {
- module.default(createRender($page), { ...opts, acl$: options() });
+ module.default(createRender($page), { ...opts, acl$: options(), getFilename, getDownloadUrl });
}))),
rxjs.catchError(ctrlError()),
));
diff --git a/public/assets/pages/filespage/ctrl_submenu.js b/public/assets/pages/filespage/ctrl_submenu.js
index 05e8e292..a6000ef4 100644
--- a/public/assets/pages/filespage/ctrl_submenu.js
+++ b/public/assets/pages/filespage/ctrl_submenu.js
@@ -1,11 +1,10 @@
import { createElement, createRender, createFragment, onDestroy } from "../../lib/skeleton/index.js";
import rxjs, { effect, onClick, preventDefault } from "../../lib/rx.js";
import { animate, slideXIn, slideYIn } from "../../lib/animate.js";
-import { forwardURLParams } from "../../lib/path.js";
+import { basename, forwardURLParams } from "../../lib/path.js";
import { loadCSS } from "../../helpers/loader.js";
import assert from "../../lib/assert.js";
import { qs, qsa } from "../../lib/dom.js";
-import { basename } from "../../lib/path.js";
import t from "../../locales/index.js";
import "../../components/dropdown.js";
diff --git a/public/assets/pages/viewerpage/application_3d.css b/public/assets/pages/viewerpage/application_3d.css
index a2638635..4edca688 100644
--- a/public/assets/pages/viewerpage/application_3d.css
+++ b/public/assets/pages/viewerpage/application_3d.css
@@ -1,3 +1,47 @@
-.component_3dviewer {
- background: #525659;
+.component_3dviewer .threeviewer_container {
+ position: relative;
+ overflow: hidden;
+}
+
+.component_3dviewer .threeviewer_container, .component_3dviewer .threeviewer_container .drawarea {
+ height: 100%;
+}
+
+.component_3dviewer .toolbar.open {
+ transition: 0.3s ease transform;
+ transform: translateX(0px);
+ height: 300px;
+}
+.component_3dviewer .toolbar {
+ position: absolute;
+ top: 0;
+ right: 0;
+ transform: translateX(250px);
+ transition: 0.1s ease transform;
+ border-left: 1px solid #e2e2e205;
+ background: #e2e2e205;
+ color: var(--dark);
+ width: 250px;
+ z-index: 1;
+ padding-top: 20px;
+}
+
+.component_3dviewer .toolbar label {
+ display: block;
+ text-transform: capitalize;
+ margin-bottom: -5px;
+}
+
+.component_3dviewer .toolbar label .component_checkbox {
+ margin-top: -10px;
+}
+
+.component_3dviewer .toolbar label .text {
+ margin-left: -10px;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: calc(100% - 25px);
+ cursor: pointer;
}
diff --git a/public/assets/pages/viewerpage/application_3d.js b/public/assets/pages/viewerpage/application_3d.js
index a57d42b7..8e0e3edd 100644
--- a/public/assets/pages/viewerpage/application_3d.js
+++ b/public/assets/pages/viewerpage/application_3d.js
@@ -1,38 +1,42 @@
-import { createElement, onDestroy } from "../../lib/skeleton/index.js";
+import { createElement, createRender, nop } from "../../lib/skeleton/index.js";
import rxjs, { effect } from "../../lib/rx.js";
import { qs } from "../../lib/dom.js";
import { loadCSS } from "../../helpers/loader.js";
-import { join } from "../../lib/path.js";
import { createLoader } from "../../components/loader.js";
-import { getFilename, getDownloadUrl } from "./common.js";
-
-import * as THREE from "../../lib/vendor/three/three.module.js";
-import { OrbitControls } from "../../lib/vendor/three/OrbitControls.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";
+import ctrlError from "../ctrl_error.js";
import componentDownloader, { init as initDownloader } from "./application_downloader.js";
-
import { renderMenubar, buttonDownload } from "./component_menubar.js";
-export default function(render, { mime }) {
+import setup3D, { getLoader } from "./application_3d/init.js";
+import withLight from "./application_3d/scene_light.js";
+import withCube from "./application_3d/scene_cube.js";
+import ctrlToolbar from "./application_3d/toolbar.js";
+
+export default async function(render, { mime, acl$, getDownloadUrl = nop, getFilename = nop, hasCube = true, hasMenubar = true }) {
const $page = createElement(`
`);
render($page);
- renderMenubar(qs($page, "component-menubar"), buttonDownload(getFilename(), getDownloadUrl()));
- const removeLoader = createLoader(qs($page, ".threeviewer_container"));
- effect(rxjs.of(getLoader(mime)).pipe(
+ const $menubar = renderMenubar(
+ qs($page, "component-menubar"),
+ buttonDownload(getFilename(), getDownloadUrl()),
+ );
+ const $draw = qs($page, ".drawarea");
+ const $toolbar = qs($page, ".toolbar");
+
+ const removeLoader = createLoader($draw);
+ await effect(rxjs.of(getLoader(mime)).pipe(
rxjs.mergeMap(([loader, createMesh]) => {
if (!loader) {
- componentDownloader(render);
+ componentDownloader(render, { mime, acl$ });
return rxjs.EMPTY;
}
return rxjs.of([loader, createMesh]);
@@ -44,81 +48,46 @@ export default function(render, { mime }) {
(err) => observer.error(err),
))),
removeLoader,
- rxjs.mergeMap((mesh) => {
- // setup the dom
- const renderer = new THREE.WebGLRenderer();
- renderer.setSize($page.clientWidth, $page.clientHeight);
- renderer.setClearColor(0x525659);
- qs($page, ".threeviewer_container").appendChild(renderer.domElement);
-
- // setup the scene
- const scene = new THREE.Scene();
- scene.add(mesh);
-
- // setup the main threeJS components: camera, controls & lighting
- const camera = new THREE.PerspectiveCamera(45, $page.clientWidth / $page.clientHeight, 1, 1000);
- const controls = new OrbitControls(camera, renderer.domElement);
- [
- new THREE.AmbientLight(0xffffff, 1.5),
- new THREE.DirectionalLight(0xffffff, 1.5),
- new THREE.DirectionalLight(0xffffff, 1.5),
- ].forEach((light, i) => {
- if (i === 1) light.position.set(100, 100, 100);
- else if (i === 2) light.position.set(-100, -100, -100);
- scene.add(light);
- });
-
- // center everything
- const box = new THREE.Box3().setFromObject(mesh);
- const center = box.getCenter(new THREE.Vector3());
- const size = box.getSize(new THREE.Vector3());
- camera.position.set(center.x, center.y, center.z + Math.max(size.x, size.y, size.z) * 1.8);
- controls.target.copy(center);
-
- // resize handler
- 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));
-
- return rxjs.animationFrames().pipe(rxjs.tap(() => {
- controls.update();
- renderer.render(scene, camera);
- }));
- }),
+ rxjs.mergeMap((mesh) => create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube })),
+ rxjs.catchError(ctrlError()),
));
}
-function getLoader(mime) {
- const identity = (s) => s;
- switch (mime) {
- case "application/object":
- return [new OBJLoader(), identity];
- case "model/3dm":
- 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) => new THREE.Mesh(
- geometry,
- new THREE.MeshPhongMaterial(),
- )];
- case "application/fbx":
- return [new FBXLoader(), identity];
- default:
- return [null, null];
- }
+function create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube }) {
+ const refresh = [];
+ const { renderer, camera, scene, controls, box } = setup3D({
+ $page: $draw,
+ mesh,
+ refresh,
+ $menubar,
+ });
+
+ withLight({ scene, box });
+ if (hasCube) withCube({ camera, renderer, refresh, controls });
+ ctrlToolbar(createRender($toolbar), {
+ mesh,
+ controls,
+ camera,
+ refresh,
+ $menubar,
+ $toolbar,
+ });
+
+ return rxjs.animationFrames().pipe(rxjs.tap(() => {
+ refresh.forEach((fn) => fn());
+ }));
}
-export function init() {
+export function init($root) {
+ const priors = ($root && [
+ $root.classList.add("component_page_viewerpage"),
+ loadCSS(import.meta.url, "./component_menubar.css"),
+ loadCSS(import.meta.url, "../ctrl_viewerpage.css"),
+ ]);
+
return Promise.all([
loadCSS(import.meta.url, "./application_3d.css"),
initDownloader(),
+ ...priors,
]);
}
diff --git a/public/assets/pages/viewerpage/application_3d/init.js b/public/assets/pages/viewerpage/application_3d/init.js
new file mode 100644
index 00000000..00034cda
--- /dev/null
+++ b/public/assets/pages/viewerpage/application_3d/init.js
@@ -0,0 +1,137 @@
+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(`

`);
+ 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];
+ }
+}
diff --git a/public/assets/pages/viewerpage/application_3d/scene_cube.js b/public/assets/pages/viewerpage/application_3d/scene_cube.js
new file mode 100644
index 00000000..4f428005
--- /dev/null
+++ b/public/assets/pages/viewerpage/application_3d/scene_cube.js
@@ -0,0 +1,23 @@
+import { onDestroy } from "../../../lib/skeleton/index.js";
+import { ViewCubeGizmo, SimpleCameraControls, ObjectPosition } from "../../../lib/vendor/three/viewcube.js";
+
+export default function({ camera, renderer, refresh, controls }) {
+ const viewCubeGizmo = new ViewCubeGizmo(camera, renderer, {
+ pos: ObjectPosition.RIGHT_BOTTOM,
+ dimension: 130,
+ faceColor: 0xf9f9fa,
+ outlineColor: 0xe2e2e2,
+ });
+
+ const simpleCameraControls = new SimpleCameraControls(camera);
+ simpleCameraControls.setControls(controls);
+
+ refresh.push(() => {
+ viewCubeGizmo.update();
+ simpleCameraControls.update();
+ });
+
+ const onCubeClick = (event) => simpleCameraControls.flyTo(event.quaternion);
+ viewCubeGizmo.addEventListener("change", onCubeClick);
+ onDestroy(() => viewCubeGizmo.removeEventListener("change", onCubeClick));
+}
diff --git a/public/assets/pages/viewerpage/application_3d/scene_light.js b/public/assets/pages/viewerpage/application_3d/scene_light.js
new file mode 100644
index 00000000..85fb8385
--- /dev/null
+++ b/public/assets/pages/viewerpage/application_3d/scene_light.js
@@ -0,0 +1,38 @@
+import { settings_get } from "../../../lib/settings.js";
+import * as THREE from "../../../lib/vendor/three/three.module.js";
+
+const LIGHT_COLOR = 0xf5f5f5;
+
+export default function({ scene, box }) {
+ addLight(
+ scene,
+ new THREE.AmbientLight(LIGHT_COLOR),
+ settings_get("viewerpage_3d_light", 2),
+ );
+
+ // to make things "look nice", a good setup is to get lights positioned
+ // in a 3D cube with a couple "twist" in term of position & intensity
+ const l = addLight.bind(this, scene, new THREE.DirectionalLight(LIGHT_COLOR));
+ l(0.25, [plus(box.max.x*3), 0, 20]); // right
+ l(0.25, [minus(box.min.x*3), 0, -20]); // left
+ l(0.35, [0, plus(box.max.y*4), 20]); // top
+ l(0.35, [0, minus(box.min.y*4), -20]); // bottom
+
+ l(0.5, [0, 0, plus(7*box.max.z)]); // front
+ l(0.2, [0, 0, minus(15*box.min.z)]); // back
+}
+
+function addLight(scene, light, intensity, pos = []) {
+ light = light.clone();
+ light.intensity = intensity;
+ light.position.set(...pos);
+ if (light.type !== "AmbientLight") light.castShadow = true;
+ scene.add(light);
+}
+
+const plus = notZero.bind(null, 1);
+const minus = notZero.bind(null, -1);
+function notZero(sgn, n) {
+ if (n === 0) return sgn;
+ return n;
+}
diff --git a/public/assets/pages/viewerpage/application_3d/toolbar.js b/public/assets/pages/viewerpage/application_3d/toolbar.js
new file mode 100644
index 00000000..fbcdf602
--- /dev/null
+++ b/public/assets/pages/viewerpage/application_3d/toolbar.js
@@ -0,0 +1,97 @@
+import { createElement } from "../../../lib/skeleton/index.js";
+import { qs } from "../../../lib/dom.js";
+import * as THREE from "../../../lib/vendor/three/three.module.js";
+
+export default function(render, { camera, controls, mesh, $menubar, $toolbar }) {
+ if (mesh.children.length <= 1) return;
+
+ $menubar.add(buttonLayers({ $toolbar }));
+ render(createChild(
+ document.createDocumentFragment(),
+ mesh,
+ 0,
+ { camera, controls }
+ ));
+}
+
+function buttonLayers({ $toolbar }) {
+ const $button = createElement(`

`);
+ $button.onclick = () => $toolbar.classList.toggle("open");
+ return $button;
+}
+
+function createChild($fragment, mesh, child = 0, opts) {
+ if (["Bone"].indexOf(mesh.type) >= 0) return;
+ buildDOM($fragment, mesh, child, opts);
+ if (mesh.children.length > 0 && child < 4) {
+ for (let i=0; i
+
+
+
+
+ ${name(child)}
+
+ `);
+ qs($label, "input").onchange = () => child.visible = !child.visible;
+ let block = false; let blockID = null;
+ $label.onclick = async(e) => {
+ if (e.target.nodeName === "INPUT" || e.target.classList.contains("component_checkbox")) return;
+ e.preventDefault(); e.stopPropagation();
+ block = true;
+ clearTimeout(blockID);
+ blockID = setTimeout(() => block = false, 2000);
+ $label.onmouseenter();
+ await flyTo({ mesh: child, camera, controls });
+ };
+ $label.onmouseenter = () => block === false && getRootObject(child).traverse((c) => {
+ if (!c.material) return;
+ c.material.opacity = c.id === child.id || c.parent.id === child.id ? 1 : 0.2;
+ c.material.depthWrite = c.material.opacity === 1;
+ });
+ $label.onmouseleave = () => block === false && getRootObject(child).traverse((c) => {
+ if (!c.material) return;
+ c.material.depthWrite = true;
+ c.material.opacity = 1;
+ });
+ $fragment.appendChild($label);
+}
+
+async function flyTo({ mesh, camera, controls }) {
+ const box = new THREE.Box3().setFromObject(mesh);
+ const size = box.getSize(new THREE.Vector3());
+
+ const targetLookAt = box.getCenter(new THREE.Vector3());
+ const targetDistance = Math.max(size.x, size.y, size.z) * 1.1;
+ const targetPosition = targetLookAt.clone().add(new THREE.Vector3(targetDistance, targetDistance, targetDistance));
+
+ const [startPosition, startLookAt] = [camera.position.clone(), controls.target.clone()];
+ const startTime = performance.now();
+ return new Promise((resolve) => (function animate() {
+ const t = Math.min((performance.now() - startTime) / 500, 1);
+ camera.position.lerpVectors(startPosition, targetPosition, t);
+ controls.target.lerpVectors(startLookAt, targetLookAt, t);
+ controls.update();
+ t < 1 ? requestAnimationFrame(animate) : resolve();
+ })());
+}
+
+function getRootObject(mesh) {
+ if (mesh.type === "Scene" || mesh.parent.type === "Scene") return mesh;
+ return getRootObject(mesh.parent);
+}
+
+function name(mesh) {
+ if (mesh.name) return mesh.name;
+ else if (mesh.isGroup && mesh.uuid) return `group: ${mesh.uuid}`;
+ else if (mesh.uuid) return mesh.uuid;
+ return "N/A";
+}
diff --git a/public/assets/pages/viewerpage/common.js b/public/assets/pages/viewerpage/common.js
index 0a81bcfb..49ee10f0 100644
--- a/public/assets/pages/viewerpage/common.js
+++ b/public/assets/pages/viewerpage/common.js
@@ -7,7 +7,7 @@ export function transition($node) {
}
export function getFilename() {
- return basename(getCurrentPath()) || "untitled.dat";
+ return basename(getCurrentPath()) || " ";
}
export function getDownloadUrl() {
diff --git a/public/assets/pages/viewerpage/component_menubar.js b/public/assets/pages/viewerpage/component_menubar.js
index d3d2fc85..7e5cc259 100644
--- a/public/assets/pages/viewerpage/component_menubar.js
+++ b/public/assets/pages/viewerpage/component_menubar.js
@@ -2,7 +2,7 @@ import { createElement } from "../../lib/skeleton/index.js";
import { qs } from "../../lib/dom.js";
import { animate, slideYIn } from "../../lib/animate.js";
import { loadCSS } from "../../helpers/loader.js";
-import { getFilename } from "./common.js";
+import { isSDK } from "../../helpers/sdk.js";
import assert from "../../lib/assert.js";
import "../../components/dropdown.js";
@@ -14,7 +14,7 @@ export default class ComponentMenubar extends HTMLElement {
this.innerHTML = `
- ${getFilename()}
+ ${this.getAttribute("filename") || " "}
@@ -45,6 +45,12 @@ export default class ComponentMenubar extends HTMLElement {
}
animate($item, { time: 250, keyframes: slideYIn(2) });
}
+
+ add($button) {
+ const $item = assert.type(this.querySelector(".action-item"), HTMLElement);
+ $item.prepend($button);
+ animate($button, { time: 250, keyframes: slideYIn(2) });
+ }
}
export function buttonDownload(name, link) {
@@ -61,6 +67,7 @@ export function buttonDownload(name, link) {
`);
const $img = qs($el, "img");
qs($el, "a").onclick = () => {
+ if (isSDK()) return;
document.cookie = "download=yes; path=/; max-age=120;";
$img.setAttribute("src", ICON.LOADING);
const id = setInterval(() => {
@@ -92,6 +99,7 @@ export function buttonFullscreen($screen) {
export function renderMenubar($menubar, ...buttons) {
assert.type($menubar, ComponentMenubar);
$menubar.render(buttons.filter(($button) => $button));
+ return $menubar;
}
export async function init() {
diff --git a/server/middleware/http.go b/server/middleware/http.go
index 39bcdcc0..81ffac82 100644
--- a/server/middleware/http.go
+++ b/server/middleware/http.go
@@ -32,6 +32,20 @@ func StaticHeaders(fn HandlerFunc) HandlerFunc {
})
}
+func PublicCORS(fn HandlerFunc) HandlerFunc {
+ return HandlerFunc(func(ctx *App, res http.ResponseWriter, req *http.Request) {
+ header := res.Header()
+ header.Set("Access-Control-Allow-Origin", "*")
+ header.Set("Access-Control-Allow-Headers", "x-requested-with")
+ if req.Method == http.MethodOptions {
+ header.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
+ res.WriteHeader(http.StatusNoContent)
+ return
+ }
+ fn(ctx, res, req)
+ })
+}
+
func IndexHeaders(fn HandlerFunc) HandlerFunc {
return HandlerFunc(func(ctx *App, res http.ResponseWriter, req *http.Request) {
header := res.Header()
diff --git a/server/routes.go b/server/routes.go
index ea62000e..9a54a7d2 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -90,15 +90,15 @@ func Build(a App) *mux.Router {
// Application Resources
middlewares = []Middleware{ApiHeaders, SecureHeaders, PluginInjector}
- r.HandleFunc(WithBase("/api/config"), NewMiddlewareChain(PublicConfigHandler, middlewares, a)).Methods("GET")
r.HandleFunc(WithBase("/api/backend"), NewMiddlewareChain(AdminBackend, middlewares, a)).Methods("GET")
- middlewares = []Middleware{StaticHeaders, SecureHeaders, PluginInjector}
+ r.HandleFunc(WithBase("/api/config"), NewMiddlewareChain(PublicConfigHandler, append(middlewares, PublicCORS), a)).Methods("GET", "OPTIONS")
+ middlewares = []Middleware{StaticHeaders, SecureHeaders, PublicCORS, PluginInjector}
if os.Getenv("CANARY") == "" { // TODO: remove after migration is done
- r.PathPrefix(WithBase("/assets")).Handler(http.HandlerFunc(NewMiddlewareChain(LegacyStaticHandler("/"), middlewares, a))).Methods("GET")
+ r.PathPrefix(WithBase("/assets")).Handler(http.HandlerFunc(NewMiddlewareChain(LegacyStaticHandler("/"), middlewares, a))).Methods("GET", "OPTIONS")
r.HandleFunc(WithBase("/favicon.ico"), NewMiddlewareChain(LegacyStaticHandler("/assets/logo/"), middlewares, a)).Methods("GET")
r.HandleFunc(WithBase("/sw_cache.js"), NewMiddlewareChain(LegacyStaticHandler("/assets/worker/"), middlewares, a)).Methods("GET")
} else { // TODO: remove this after migration is done
- r.PathPrefix(WithBase("/assets")).Handler(http.HandlerFunc(NewMiddlewareChain(ServeFile("/"), middlewares, a))).Methods("GET")
+ r.PathPrefix(WithBase("/assets")).Handler(http.HandlerFunc(NewMiddlewareChain(ServeFile("/"), middlewares, a))).Methods("GET", "OPTIONS")
r.HandleFunc(WithBase("/favicon.ico"), NewMiddlewareChain(ServeFavicon, middlewares, a)).Methods("GET")
}