mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-27 11:35:04 +08:00
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
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
15
public/assets/helpers/sdk.js
Normal file
15
public/assets/helpers/sdk.js
Normal file
@ -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;
|
||||
}
|
||||
54
public/assets/index.js
Normal file
54
public/assets/index.js
Normal file
@ -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(`
|
||||
<div style="position: absolute; bottom: 0; left: 0; font-size: 0.9rem; display: inline-block;">
|
||||
Powered By <a href="https://www.filestash.app" style="text-decoration:underline;">Filestash</a>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import rxjs, { ajax } from "./rx.js";
|
||||
import { AjaxError } from "./error.js";
|
||||
import { isSDK, urlSDK } from "../helpers/sdk.js";
|
||||
|
||||
export default function(opts) {
|
||||
if (typeof opts === "string") opts = { url: opts, withCredentials: true };
|
||||
@ -7,6 +8,12 @@ export default function(opts) {
|
||||
if (!opts.headers) opts.headers = {};
|
||||
opts.headers["X-Requested-With"] = "XmlHttpRequest";
|
||||
if (window.BEARER_TOKEN) opts.headers["Authorization"] = `Bearer ${window.BEARER_TOKEN}`;
|
||||
|
||||
if (isSDK()) {
|
||||
if (["/api/config"].indexOf(opts.url) === -1) opts.withCredentials = false;
|
||||
opts.url = urlSDK(opts.url);
|
||||
}
|
||||
|
||||
return ajax({ withCredentials: true, ...opts, responseType: "text" }).pipe(
|
||||
rxjs.map((res) => {
|
||||
const result = res.xhr.responseText;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
const settings = JSON.parse(window.localStorage.getItem("settings") || "null") || {};
|
||||
|
||||
export function settings_get(key) {
|
||||
export function settings_get(key, def = null) {
|
||||
if (settings[key] === undefined) {
|
||||
return null;
|
||||
return def;
|
||||
}
|
||||
return settings[key];
|
||||
}
|
||||
|
||||
899
public/assets/lib/vendor/three/viewcube.js
vendored
Normal file
899
public/assets/lib/vendor/three/viewcube.js
vendored
Normal file
@ -0,0 +1,899 @@
|
||||
import * as THREE from "./three.module.js";
|
||||
const DEFAULT_FACENAMES = {
|
||||
top: "TOP",
|
||||
front: "FRONT",
|
||||
right: "RIGHT",
|
||||
back: "BACK",
|
||||
left: "LEFT",
|
||||
bottom: "BOTTOM"
|
||||
};
|
||||
var ObjectPosition = /* @__PURE__ */ ((ObjectPosition2) => {
|
||||
ObjectPosition2[ObjectPosition2["LEFT_BOTTOM"] = 0] = "LEFT_BOTTOM";
|
||||
ObjectPosition2[ObjectPosition2["LEFT_TOP"] = 1] = "LEFT_TOP";
|
||||
ObjectPosition2[ObjectPosition2["RIGHT_TOP"] = 2] = "RIGHT_TOP";
|
||||
ObjectPosition2[ObjectPosition2["RIGHT_BOTTOM"] = 4] = "RIGHT_BOTTOM";
|
||||
return ObjectPosition2;
|
||||
})(ObjectPosition || {});
|
||||
class FixedPosGizmo extends THREE.Object3D {
|
||||
/**
|
||||
* Construct one instance of this gizmo
|
||||
* @param camera Camera used in your canvas
|
||||
* @param renderer Renderer used in your canvas
|
||||
* @param dimension Size of area ocupied by this gizmo. Because width and height of this area is same,
|
||||
* it is single value. The real size of the objet will be calculated automatically considering rotation.
|
||||
* @param pos Position of the gizmo
|
||||
*/
|
||||
constructor(camera, renderer, dimension = 150, pos = 2) {
|
||||
super();
|
||||
this.camera = camera;
|
||||
this.renderer = renderer;
|
||||
this.gizmoCamera = new THREE.OrthographicCamera(-2, 2, 2, -2, 0, 4);
|
||||
this.gizmoCamera.position.set(0, 0, 2);
|
||||
this.gizmoDim = dimension;
|
||||
this.gizmoPos = pos;
|
||||
this.initialize();
|
||||
}
|
||||
/**
|
||||
* Function called by constructor to initialize this gizmo. The children class can override this function
|
||||
* to add its own initialization logic.
|
||||
*/
|
||||
initialize() {
|
||||
}
|
||||
/**
|
||||
* Update and rerender this gizmo
|
||||
*/
|
||||
update() {
|
||||
this.updateOrientation();
|
||||
const autoClear = this.renderer.autoClear;
|
||||
this.renderer.autoClear = false;
|
||||
this.renderer.clearDepth();
|
||||
const viewport = new THREE.Vector4();
|
||||
this.renderer.getViewport(viewport);
|
||||
const pos = this.calculateViewportPos();
|
||||
this.renderer.setViewport(pos.x, pos.y, this.gizmoDim, this.gizmoDim);
|
||||
this.renderer.render(this, this.gizmoCamera);
|
||||
this.renderer.setViewport(viewport.x, viewport.y, viewport.z, viewport.w);
|
||||
this.renderer.autoClear = autoClear;
|
||||
}
|
||||
/**
|
||||
* Free the GPU-related resources allocated by this instance. Call this method whenever this instance
|
||||
* is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
}
|
||||
updateOrientation() {
|
||||
this.quaternion.copy(this.camera.quaternion).invert();
|
||||
this.updateMatrixWorld();
|
||||
}
|
||||
calculatePosInViewport(offsetX, offsetY, bbox) {
|
||||
const x = (offsetX - bbox.min.x) / this.gizmoDim * 2 - 1;
|
||||
const y = -((offsetY - bbox.min.y) / this.gizmoDim) * 2 + 1;
|
||||
return { x, y };
|
||||
}
|
||||
calculateViewportPos() {
|
||||
const domElement = this.renderer.domElement;
|
||||
const canvasWidth = domElement.offsetWidth;
|
||||
const canvasHeight = domElement.offsetHeight;
|
||||
const pos = this.gizmoPos;
|
||||
const length = this.gizmoDim;
|
||||
let x = canvasWidth - length;
|
||||
let y = canvasHeight - length;
|
||||
switch (pos) {
|
||||
case 0:
|
||||
x = 0;
|
||||
y = 0;
|
||||
break;
|
||||
case 1:
|
||||
x = 0;
|
||||
break;
|
||||
case 4:
|
||||
y = 0;
|
||||
break;
|
||||
}
|
||||
return { x, y };
|
||||
}
|
||||
calculateViewportBbox() {
|
||||
const domElement = this.renderer.domElement;
|
||||
const canvasWidth = domElement.offsetWidth;
|
||||
const canvasHeight = domElement.offsetHeight;
|
||||
const pos = this.gizmoPos;
|
||||
const length = this.gizmoDim;
|
||||
const bbox = new THREE.Box2(
|
||||
new THREE.Vector2(canvasWidth - length, 0),
|
||||
new THREE.Vector2(canvasWidth, length)
|
||||
);
|
||||
switch (pos) {
|
||||
case 0:
|
||||
bbox.set(
|
||||
new THREE.Vector2(0, canvasHeight - length),
|
||||
new THREE.Vector2(length, canvasHeight)
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
bbox.set(new THREE.Vector2(0, 0), new THREE.Vector2(length, length));
|
||||
break;
|
||||
case 4:
|
||||
bbox.set(
|
||||
new THREE.Vector2(canvasWidth - length, canvasHeight - length),
|
||||
new THREE.Vector2(canvasWidth, canvasHeight)
|
||||
);
|
||||
break;
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
}
|
||||
function createTextTexture(text, props) {
|
||||
const fontface = props.font || "Helvetica";
|
||||
const fontsize = props.fontSize || 30;
|
||||
const width = props.width || 200;
|
||||
const height = props.height || 200;
|
||||
const bgColor = props.bgColor ? props.bgColor.join(", ") : "255, 255, 255, 1.0";
|
||||
const fgColor = props.color ? props.color.join(", ") : "0, 0, 0, 1.0";
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext("2d");
|
||||
if (context) {
|
||||
context.font = `bold ${fontsize}px ${fontface}`;
|
||||
context.fillStyle = `rgba(${bgColor})`;
|
||||
context.fillRect(0, 0, width, height);
|
||||
const metrics = context.measureText(text);
|
||||
const textWidth = metrics.width;
|
||||
context.fillStyle = `rgba(${fgColor})`;
|
||||
context.fillText(
|
||||
text,
|
||||
width / 2 - textWidth / 2,
|
||||
height / 2 + fontsize / 2 - 2
|
||||
);
|
||||
}
|
||||
const texture = new THREE.Texture(canvas);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
function createTextSprite(text) {
|
||||
const texture = createTextTexture(text, {
|
||||
fontSize: 100,
|
||||
font: "Arial Narrow, sans-serif",
|
||||
color: [255, 255, 255, 1],
|
||||
bgColor: [0, 0, 0, 0]
|
||||
});
|
||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||||
return new THREE.Sprite(material);
|
||||
}
|
||||
function createFaceMaterials(faceNames = DEFAULT_FACENAMES) {
|
||||
const materials = [
|
||||
{
|
||||
name: FACES.FRONT,
|
||||
map: createTextTexture(faceNames.front, {
|
||||
fontSize: 55,
|
||||
font: "Arial Narrow, sans-serif",
|
||||
color: ["87", "89", "90", "1"]
|
||||
})
|
||||
},
|
||||
{
|
||||
name: FACES.RIGHT,
|
||||
map: createTextTexture(faceNames.right, {
|
||||
fontSize: 55,
|
||||
font: "Arial Narrow, sans-serif",
|
||||
color: ["87", "89", "90", "1"]
|
||||
})
|
||||
},
|
||||
{
|
||||
name: FACES.BACK,
|
||||
map: createTextTexture(faceNames.back, {
|
||||
fontSize: 55,
|
||||
font: "Arial Narrow, sans-serif",
|
||||
color: ["87", "89", "90", "1"]
|
||||
})
|
||||
},
|
||||
{
|
||||
name: FACES.LEFT,
|
||||
map: createTextTexture(faceNames.left, {
|
||||
fontSize: 55,
|
||||
font: "Arial Narrow, sans-serif",
|
||||
color: ["87", "89", "90", "1"]
|
||||
})
|
||||
},
|
||||
{
|
||||
name: FACES.TOP,
|
||||
map: createTextTexture(faceNames.top, {
|
||||
fontSize: 60,
|
||||
font: "Arial Narrow, sans-serif",
|
||||
color: ["87", "89", "90", "1"]
|
||||
})
|
||||
},
|
||||
{
|
||||
name: FACES.BOTTOM,
|
||||
map: createTextTexture(faceNames.bottom, {
|
||||
fontSize: 48,
|
||||
font: "Arial Narrow, sans-serif",
|
||||
color: ["87", "89", "90", "1"]
|
||||
})
|
||||
}
|
||||
];
|
||||
return materials;
|
||||
}
|
||||
const FACES = {
|
||||
TOP: "1",
|
||||
FRONT: "2",
|
||||
RIGHT: "3",
|
||||
BACK: "4",
|
||||
LEFT: "5",
|
||||
BOTTOM: "6",
|
||||
TOP_FRONT_EDGE: "7",
|
||||
TOP_RIGHT_EDGE: "8",
|
||||
TOP_BACK_EDGE: "9",
|
||||
TOP_LEFT_EDGE: "10",
|
||||
FRONT_RIGHT_EDGE: "11",
|
||||
BACK_RIGHT_EDGE: "12",
|
||||
BACK_LEFT_EDGE: "13",
|
||||
FRONT_LEFT_EDGE: "14",
|
||||
BOTTOM_FRONT_EDGE: "15",
|
||||
BOTTOM_RIGHT_EDGE: "16",
|
||||
BOTTOM_BACK_EDGE: "17",
|
||||
BOTTOM_LEFT_EDGE: "18",
|
||||
TOP_FRONT_RIGHT_CORNER: "19",
|
||||
TOP_BACK_RIGHT_CORNER: "20",
|
||||
TOP_BACK_LEFT_CORNER: "21",
|
||||
TOP_FRONT_LEFT_CORNER: "22",
|
||||
BOTTOM_FRONT_RIGHT_CORNER: "23",
|
||||
BOTTOM_BACK_RIGHT_CORNER: "24",
|
||||
BOTTOM_BACK_LEFT_CORNER: "25",
|
||||
BOTTOM_FRONT_LEFT_CORNER: "26"
|
||||
};
|
||||
const CORNER_FACES = [
|
||||
{ name: FACES.TOP_FRONT_RIGHT_CORNER },
|
||||
{ name: FACES.TOP_BACK_RIGHT_CORNER },
|
||||
{ name: FACES.TOP_BACK_LEFT_CORNER },
|
||||
{ name: FACES.TOP_FRONT_LEFT_CORNER },
|
||||
{ name: FACES.BOTTOM_BACK_RIGHT_CORNER },
|
||||
{ name: FACES.BOTTOM_FRONT_RIGHT_CORNER },
|
||||
{ name: FACES.BOTTOM_FRONT_LEFT_CORNER },
|
||||
{ name: FACES.BOTTOM_BACK_LEFT_CORNER }
|
||||
];
|
||||
const EDGE_FACES = [
|
||||
{ name: FACES.TOP_FRONT_EDGE },
|
||||
{ name: FACES.TOP_RIGHT_EDGE },
|
||||
{ name: FACES.TOP_BACK_EDGE },
|
||||
{ name: FACES.TOP_LEFT_EDGE },
|
||||
// flip back and front bottom edges
|
||||
{ name: FACES.BOTTOM_BACK_EDGE },
|
||||
{ name: FACES.BOTTOM_RIGHT_EDGE },
|
||||
{ name: FACES.BOTTOM_FRONT_EDGE },
|
||||
{ name: FACES.BOTTOM_LEFT_EDGE }
|
||||
];
|
||||
const EDGE_FACES_SIDE = [
|
||||
{ name: FACES.FRONT_RIGHT_EDGE },
|
||||
{ name: FACES.BACK_RIGHT_EDGE },
|
||||
{ name: FACES.BACK_LEFT_EDGE },
|
||||
{ name: FACES.FRONT_LEFT_EDGE }
|
||||
];
|
||||
class ViewCube extends THREE.Object3D {
|
||||
/**
|
||||
* Construct one instance of view cube 3d object
|
||||
* @param cubeSize Size of area ocupied by view cube
|
||||
* @param borderSize Border size of view cube
|
||||
* @param isShowOutline Flag to decide whether to show edge of view cube
|
||||
* @param faceColor Face color of view cube
|
||||
* @param outlineColor Edge color of view cube
|
||||
* @param faceNames Texts in each face of view cube
|
||||
*/
|
||||
constructor(cubeSize = 60, borderSize = 5, isShowOutline = true, faceColor = 13421772, outlineColor = 10066329, faceNames = DEFAULT_FACENAMES) {
|
||||
super();
|
||||
this._cubeSize = cubeSize;
|
||||
this._borderSize = borderSize;
|
||||
this._isShowOutline = isShowOutline;
|
||||
this._faceColor = faceColor;
|
||||
this._outlineColor = outlineColor;
|
||||
this.build(faceNames);
|
||||
}
|
||||
/**
|
||||
* Free the GPU-related resources allocated by this instance. Call this method whenever this instance
|
||||
* is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
this.children.forEach((child) => {
|
||||
var _a, _b, _c, _d;
|
||||
const mesh = child;
|
||||
(_a = mesh.material) == null ? void 0 : _a.dispose();
|
||||
(_c = (_b = mesh.material) == null ? void 0 : _b.map) == null ? void 0 : _c.dispose();
|
||||
(_d = mesh.geometry) == null ? void 0 : _d.dispose();
|
||||
});
|
||||
}
|
||||
build(faceNames) {
|
||||
const faceSize = this._cubeSize - this._borderSize * 2;
|
||||
const faceOffset = this._cubeSize / 2;
|
||||
const borderSize = this._borderSize;
|
||||
const cubeFaces = this.createCubeFaces(faceSize, faceOffset);
|
||||
const faceMaterials = createFaceMaterials(faceNames);
|
||||
for (const [i, props] of faceMaterials.entries()) {
|
||||
const face = cubeFaces.children[i];
|
||||
const material = face.material;
|
||||
material.color.setHex(this._faceColor);
|
||||
material.map = props.map;
|
||||
face.name = props.name;
|
||||
}
|
||||
this.add(cubeFaces);
|
||||
const corners = [];
|
||||
for (const [i, props] of CORNER_FACES.entries()) {
|
||||
const corner = this.createCornerFaces(
|
||||
borderSize,
|
||||
faceOffset,
|
||||
props.name,
|
||||
{ color: this._faceColor }
|
||||
);
|
||||
corner.rotateOnAxis(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
THREE.MathUtils.degToRad(i % 4 * 90)
|
||||
);
|
||||
corners.push(corner);
|
||||
}
|
||||
const topCorners = new THREE.Group();
|
||||
const bottomCorners = new THREE.Group();
|
||||
this.add(topCorners.add(...corners.slice(0, 4)));
|
||||
this.add(
|
||||
bottomCorners.add(...corners.slice(4)).rotateOnAxis(new THREE.Vector3(1, 0, 0), Math.PI)
|
||||
);
|
||||
const edges = [];
|
||||
for (const [i, props] of EDGE_FACES.entries()) {
|
||||
const edge = this.createHorzEdgeFaces(
|
||||
faceSize,
|
||||
borderSize,
|
||||
faceOffset,
|
||||
props.name,
|
||||
{ color: this._faceColor }
|
||||
);
|
||||
edge.rotateOnAxis(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
THREE.MathUtils.degToRad(i % 4 * 90)
|
||||
);
|
||||
edges.push(edge);
|
||||
}
|
||||
const topEdges = new THREE.Group();
|
||||
const bottomEdges = new THREE.Group();
|
||||
this.add(topEdges.add(...edges.slice(0, 4)));
|
||||
this.add(
|
||||
bottomEdges.add(...edges.slice(4)).rotateOnAxis(new THREE.Vector3(1, 0, 0), Math.PI)
|
||||
);
|
||||
const sideEdges = new THREE.Group();
|
||||
for (const [i, props] of EDGE_FACES_SIDE.entries()) {
|
||||
const edge = this.createVertEdgeFaces(
|
||||
borderSize,
|
||||
faceSize,
|
||||
faceOffset,
|
||||
props.name,
|
||||
{ color: this._faceColor }
|
||||
);
|
||||
edge.rotateOnAxis(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
THREE.MathUtils.degToRad(i * 90)
|
||||
);
|
||||
sideEdges.add(edge);
|
||||
}
|
||||
this.add(sideEdges);
|
||||
if (this._isShowOutline) {
|
||||
this.add(this.createCubeOutline(this._cubeSize));
|
||||
}
|
||||
}
|
||||
createFace(size, position, { axis = [0, 1, 0], angle = 0, name = "", matProps = {} } = {}) {
|
||||
if (!Array.isArray(size)) size = [size, size];
|
||||
const material = new THREE.MeshBasicMaterial(matProps);
|
||||
const geometry = new THREE.PlaneGeometry(size[0], size[1]);
|
||||
const face = new THREE.Mesh(geometry, material);
|
||||
face.name = name;
|
||||
face.rotateOnAxis(
|
||||
new THREE.Vector3(...axis),
|
||||
THREE.MathUtils.degToRad(angle)
|
||||
);
|
||||
face.position.set(position[0], position[1], position[2]);
|
||||
return face;
|
||||
}
|
||||
createCubeFaces(faceSize, offset) {
|
||||
const faces = new THREE.Object3D();
|
||||
faces.add(
|
||||
this.createFace(faceSize, [0, 0, offset], { axis: [0, 1, 0], angle: 0 })
|
||||
);
|
||||
faces.add(
|
||||
this.createFace(faceSize, [offset, 0, 0], { axis: [0, 1, 0], angle: 90 })
|
||||
);
|
||||
faces.add(
|
||||
this.createFace(faceSize, [0, 0, -offset], {
|
||||
axis: [0, 1, 0],
|
||||
angle: 180
|
||||
})
|
||||
);
|
||||
faces.add(
|
||||
this.createFace(faceSize, [-offset, 0, 0], {
|
||||
axis: [0, 1, 0],
|
||||
angle: 270
|
||||
})
|
||||
);
|
||||
faces.add(
|
||||
this.createFace(faceSize, [0, offset, 0], {
|
||||
axis: [1, 0, 0],
|
||||
angle: -90
|
||||
})
|
||||
);
|
||||
faces.add(
|
||||
this.createFace(faceSize, [0, -offset, 0], {
|
||||
axis: [1, 0, 0],
|
||||
angle: 90
|
||||
})
|
||||
);
|
||||
return faces;
|
||||
}
|
||||
createCornerFaces(faceSize, offset, name = "", matProps = {}) {
|
||||
const corner = new THREE.Object3D();
|
||||
const borderOffset = offset - faceSize / 2;
|
||||
corner.add(
|
||||
this.createFace(faceSize, [borderOffset, borderOffset, offset], {
|
||||
axis: [0, 1, 0],
|
||||
angle: 0,
|
||||
matProps,
|
||||
name
|
||||
})
|
||||
);
|
||||
corner.add(
|
||||
this.createFace(faceSize, [offset, borderOffset, borderOffset], {
|
||||
axis: [0, 1, 0],
|
||||
angle: 90,
|
||||
matProps,
|
||||
name
|
||||
})
|
||||
);
|
||||
corner.add(
|
||||
this.createFace(faceSize, [borderOffset, offset, borderOffset], {
|
||||
axis: [1, 0, 0],
|
||||
angle: -90,
|
||||
matProps,
|
||||
name
|
||||
})
|
||||
);
|
||||
return corner;
|
||||
}
|
||||
createHorzEdgeFaces(w, h, offset, name = "", matProps = {}) {
|
||||
const edge = new THREE.Object3D();
|
||||
const borderOffset = offset - h / 2;
|
||||
edge.add(
|
||||
this.createFace([w, h], [0, borderOffset, offset], {
|
||||
axis: [0, 1, 0],
|
||||
angle: 0,
|
||||
name,
|
||||
matProps
|
||||
})
|
||||
);
|
||||
edge.add(
|
||||
this.createFace([w, h], [0, offset, borderOffset], {
|
||||
axis: [1, 0, 0],
|
||||
angle: -90,
|
||||
name,
|
||||
matProps
|
||||
})
|
||||
);
|
||||
return edge;
|
||||
}
|
||||
createVertEdgeFaces(w, h, offset, name = "", matProps = {}) {
|
||||
const edge = new THREE.Object3D();
|
||||
const borderOffset = offset - w / 2;
|
||||
edge.add(
|
||||
this.createFace([w, h], [borderOffset, 0, offset], {
|
||||
axis: [0, 1, 0],
|
||||
angle: 0,
|
||||
name,
|
||||
matProps
|
||||
})
|
||||
);
|
||||
edge.add(
|
||||
this.createFace([w, h], [offset, 0, borderOffset], {
|
||||
axis: [0, 1, 0],
|
||||
angle: 90,
|
||||
name,
|
||||
matProps
|
||||
})
|
||||
);
|
||||
return edge;
|
||||
}
|
||||
createCubeOutline(size) {
|
||||
const geometry = new THREE.BoxGeometry(size, size, size);
|
||||
const geo = new THREE.EdgesGeometry(geometry);
|
||||
const mat = new THREE.LineBasicMaterial({
|
||||
color: this._outlineColor,
|
||||
linewidth: 1
|
||||
});
|
||||
const wireframe = new THREE.LineSegments(geo, mat);
|
||||
return wireframe;
|
||||
}
|
||||
}
|
||||
const MAIN_COLOR = 0xf9f9fa;
|
||||
const HOVER_COLOR = 0xececec;
|
||||
const OUTLINE_COLOR = 13421772;
|
||||
const DEFAULT_VIEWCUBE_OPTIONS = {
|
||||
pos: ObjectPosition.RIGHT_TOP,
|
||||
dimension: 150,
|
||||
faceColor: MAIN_COLOR,
|
||||
hoverColor: HOVER_COLOR,
|
||||
outlineColor: OUTLINE_COLOR,
|
||||
faceNames: DEFAULT_FACENAMES
|
||||
};
|
||||
class ViewCubeGizmo extends FixedPosGizmo {
|
||||
/**
|
||||
* Construct one instance of view cube gizmo
|
||||
* @param camera Camera used in your canvas
|
||||
* @param renderer Renderer used in your canvas
|
||||
* @param options Options to customize view cube gizmo
|
||||
*/
|
||||
constructor(camera, renderer, options = DEFAULT_VIEWCUBE_OPTIONS) {
|
||||
const mergedOptions = {
|
||||
...DEFAULT_VIEWCUBE_OPTIONS,
|
||||
...options
|
||||
};
|
||||
super(camera, renderer, options.dimension, options.pos);
|
||||
this.cube = new ViewCube(
|
||||
2,
|
||||
0.2,
|
||||
true,
|
||||
mergedOptions.faceColor,
|
||||
mergedOptions.outlineColor,
|
||||
mergedOptions.faceNames
|
||||
);
|
||||
this.add(this.cube);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleMouseClick = this.handleMouseClick.bind(this);
|
||||
this.listen(renderer.domElement);
|
||||
}
|
||||
/**
|
||||
* Free the GPU-related resources allocated by this instance. Call this method whenever this instance
|
||||
* is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
this.cube.dispose();
|
||||
}
|
||||
listen(domElement) {
|
||||
domElement.addEventListener("mousemove", this.handleMouseMove);
|
||||
domElement.addEventListener("click", this.handleMouseClick);
|
||||
}
|
||||
handleMouseClick(event) {
|
||||
const bbox = this.calculateViewportBbox();
|
||||
if (bbox.containsPoint(new THREE.Vector2(event.offsetX, event.offsetY))) {
|
||||
const pos = this.calculatePosInViewport(
|
||||
event.offsetX,
|
||||
event.offsetY,
|
||||
bbox
|
||||
);
|
||||
this.checkSideTouch(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
handleMouseMove(event) {
|
||||
const bbox = this.calculateViewportBbox();
|
||||
if (bbox.containsPoint(new THREE.Vector2(event.offsetX, event.offsetY))) {
|
||||
const pos = this.calculatePosInViewport(
|
||||
event.offsetX,
|
||||
event.offsetY,
|
||||
bbox
|
||||
);
|
||||
this.checkSideOver(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
checkSideTouch(x, y) {
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(new THREE.Vector2(x, y), this.gizmoCamera);
|
||||
const intersects = raycaster.intersectObjects(this.cube.children, true);
|
||||
if (intersects.length) {
|
||||
for (const { object } of intersects) {
|
||||
if (object.name) {
|
||||
const quaternion = this.getRotation(object.name);
|
||||
this.dispatchEvent({
|
||||
type: "change",
|
||||
quaternion
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
checkSideOver(x, y) {
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(new THREE.Vector2(x, y), this.gizmoCamera);
|
||||
const intersects = raycaster.intersectObjects(this.cube.children, true);
|
||||
this.cube.traverse(function(obj) {
|
||||
if (obj.name) {
|
||||
const mesh = obj;
|
||||
mesh.material.color.setHex(MAIN_COLOR);
|
||||
}
|
||||
});
|
||||
if (intersects.length) {
|
||||
for (const { object } of intersects) {
|
||||
if (object.name) {
|
||||
object.parent.children.forEach(function(child) {
|
||||
if (child.name === object.name) {
|
||||
const mesh = child;
|
||||
mesh.material.color.setHex(
|
||||
HOVER_COLOR
|
||||
);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getRotation(side) {
|
||||
const targetQuaternion = new THREE.Quaternion();
|
||||
switch (side) {
|
||||
case FACES.FRONT:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler());
|
||||
break;
|
||||
case FACES.RIGHT:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(0, Math.PI * 0.5, 0));
|
||||
break;
|
||||
case FACES.BACK:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(0, Math.PI, 0));
|
||||
break;
|
||||
case FACES.LEFT:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(0, -Math.PI * 0.5, 0));
|
||||
break;
|
||||
case FACES.TOP:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(-Math.PI * 0.5, 0, 0));
|
||||
break;
|
||||
case FACES.BOTTOM:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(Math.PI * 0.5, 0, 0));
|
||||
break;
|
||||
case FACES.TOP_FRONT_EDGE:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(-Math.PI * 0.25, 0, 0));
|
||||
break;
|
||||
case FACES.TOP_RIGHT_EDGE:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(-Math.PI * 0.25, Math.PI * 0.5, 0, "YXZ")
|
||||
);
|
||||
break;
|
||||
case FACES.TOP_BACK_EDGE:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(-Math.PI * 0.25, Math.PI, 0, "YXZ")
|
||||
);
|
||||
break;
|
||||
case FACES.TOP_LEFT_EDGE:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(-Math.PI * 0.25, -Math.PI * 0.5, 0, "YXZ")
|
||||
);
|
||||
break;
|
||||
case FACES.BOTTOM_FRONT_EDGE:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(Math.PI * 0.25, 0, 0));
|
||||
break;
|
||||
case FACES.BOTTOM_RIGHT_EDGE:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(Math.PI * 0.25, Math.PI * 0.5, 0, "YXZ")
|
||||
);
|
||||
break;
|
||||
case FACES.BOTTOM_BACK_EDGE:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(Math.PI * 0.25, Math.PI, 0, "YXZ")
|
||||
);
|
||||
break;
|
||||
case FACES.BOTTOM_LEFT_EDGE:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(Math.PI * 0.25, -Math.PI * 0.5, 0, "YXZ")
|
||||
);
|
||||
break;
|
||||
case FACES.FRONT_RIGHT_EDGE:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(0, Math.PI * 0.25, 0));
|
||||
break;
|
||||
case FACES.BACK_RIGHT_EDGE:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(0, Math.PI * 0.75, 0));
|
||||
break;
|
||||
case FACES.BACK_LEFT_EDGE:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(0, -Math.PI * 0.75, 0));
|
||||
break;
|
||||
case FACES.FRONT_LEFT_EDGE:
|
||||
targetQuaternion.setFromEuler(new THREE.Euler(0, -Math.PI * 0.25, 0));
|
||||
break;
|
||||
case FACES.TOP_FRONT_RIGHT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(-Math.PI * 0.25, -Math.PI * 1.75, 0)
|
||||
);
|
||||
break;
|
||||
case FACES.TOP_BACK_RIGHT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(Math.PI * 0.25, -Math.PI * 1.25, 0)
|
||||
);
|
||||
break;
|
||||
case FACES.TOP_BACK_LEFT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(Math.PI * 0.25, -Math.PI * 0.75, 0)
|
||||
);
|
||||
break;
|
||||
case FACES.TOP_FRONT_LEFT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(-Math.PI * 0.25, -Math.PI * 0.25, 0)
|
||||
);
|
||||
break;
|
||||
case FACES.BOTTOM_FRONT_RIGHT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(Math.PI * 0.25, -Math.PI * 1.75, 0)
|
||||
);
|
||||
break;
|
||||
case FACES.BOTTOM_BACK_RIGHT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(-Math.PI * 0.25, -Math.PI * 1.25, 0)
|
||||
);
|
||||
break;
|
||||
case FACES.BOTTOM_BACK_LEFT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(-Math.PI * 0.25, -Math.PI * 0.75, 0)
|
||||
);
|
||||
break;
|
||||
case FACES.BOTTOM_FRONT_LEFT_CORNER:
|
||||
targetQuaternion.setFromEuler(
|
||||
new THREE.Euler(Math.PI * 0.25, -Math.PI * 0.25, 0)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
`[ViewCubeGizmo]: Invalid face, edge, or corner name '${side}'!`
|
||||
);
|
||||
break;
|
||||
}
|
||||
return targetQuaternion;
|
||||
}
|
||||
}
|
||||
const DEFAULT_AXES_OPTIONS = {
|
||||
pos: ObjectPosition.LEFT_BOTTOM,
|
||||
size: 100,
|
||||
hasZAxis: true
|
||||
};
|
||||
class AxesGizmo extends FixedPosGizmo {
|
||||
constructor(camera, renderer, options) {
|
||||
const mergedOptions = {
|
||||
...DEFAULT_AXES_OPTIONS,
|
||||
...options
|
||||
};
|
||||
super(camera, renderer, mergedOptions.size, options.pos);
|
||||
this.hasZAxis = mergedOptions.hasZAxis;
|
||||
const vertices = [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0];
|
||||
const colors = [1, 0, 0, 1, 0.6, 0, 0, 1, 0, 0.6, 1, 0];
|
||||
if (this.hasZAxis) {
|
||||
vertices.push(0, 0, 0, 0, 0, 2);
|
||||
colors.push(0, 0, 1, 0, 0.6, 1);
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(vertices, 3)
|
||||
);
|
||||
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
vertexColors: true,
|
||||
toneMapped: false
|
||||
});
|
||||
this.axes = new THREE.LineSegments(geometry, material);
|
||||
this.axes.position.set(-1, -1, -1);
|
||||
this.add(this.axes);
|
||||
this.xText = createTextSprite("X");
|
||||
this.xText.position.set(1.5, -1, -1);
|
||||
this.add(this.xText);
|
||||
this.yText = createTextSprite("Y");
|
||||
this.yText.position.set(-1, 1.5, -1);
|
||||
this.add(this.yText);
|
||||
if (this.hasZAxis) {
|
||||
this.zText = createTextSprite("Z");
|
||||
this.zText.position.set(-1, -1, 1.5);
|
||||
this.add(this.zText);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set color of x-axis and y-axis
|
||||
* @param xAxisColor color of x-axis
|
||||
* @param yAxisColor color of y-axis
|
||||
*/
|
||||
setLineColors(xAxisColor, yAxisColor) {
|
||||
const color = new THREE.Color();
|
||||
const array = this.axes.geometry.attributes.color.array;
|
||||
color.set(xAxisColor);
|
||||
color.toArray(array, 0);
|
||||
color.toArray(array, 3);
|
||||
color.set(yAxisColor);
|
||||
color.toArray(array, 6);
|
||||
color.toArray(array, 9);
|
||||
this.axes.geometry.attributes.color.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Set text color
|
||||
* @param color text color
|
||||
*/
|
||||
setTextColor(color) {
|
||||
this.xText.material.color = color;
|
||||
this.yText.material.color = color;
|
||||
}
|
||||
/**
|
||||
* Free the GPU-related resources allocated by this instance. Call this method whenever this instance
|
||||
* is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
var _a, _b;
|
||||
this.axes.geometry.dispose();
|
||||
const material = this.axes.material;
|
||||
material.dispose();
|
||||
this.xText.geometry.dispose();
|
||||
this.xText.material.dispose();
|
||||
this.yText.geometry.dispose();
|
||||
this.yText.material.dispose();
|
||||
if (this.hasZAxis) {
|
||||
(_a = this.zText) == null ? void 0 : _a.geometry.dispose();
|
||||
(_b = this.zText) == null ? void 0 : _b.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
class SimpleCameraControls {
|
||||
/**
|
||||
* Construct one instance of view cube helper
|
||||
* @param camera Camera used in your canvas
|
||||
* @param renderer Renderer used in your canvas
|
||||
* @param options Options to customize view cube helper
|
||||
*/
|
||||
constructor(camera) {
|
||||
this.camera = camera;
|
||||
this.animating = false;
|
||||
this.turnRate = 2 * Math.PI;
|
||||
this.target = new THREE.Vector3();
|
||||
this.q1 = new THREE.Quaternion();
|
||||
this.q2 = new THREE.Quaternion();
|
||||
this.radius = 0;
|
||||
this.clock = new THREE.Clock();
|
||||
}
|
||||
/**
|
||||
* Set associated obit controls
|
||||
* @param controls The associated orbit controls
|
||||
*/
|
||||
setControls(controls) {
|
||||
if (!controls) return;
|
||||
this.controls = controls;
|
||||
}
|
||||
/**
|
||||
* Animation loop
|
||||
*/
|
||||
update() {
|
||||
var _a;
|
||||
if (this.animating === false) return;
|
||||
const delta = this.clock.getDelta();
|
||||
const step = delta * this.turnRate;
|
||||
this.q1.rotateTowards(this.q2, step);
|
||||
this.camera.position.set(0, 0, 1).applyQuaternion(this.q1).multiplyScalar(this.radius).add(this.target);
|
||||
this.camera.quaternion.rotateTowards(this.q2, step);
|
||||
this.camera.updateProjectionMatrix();
|
||||
(_a = this.controls) == null ? void 0 : _a.update();
|
||||
if (this.q1.angleTo(this.q2) <= 1e-5) {
|
||||
this.animating = false;
|
||||
this.clock.stop();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fly with the target quaterion
|
||||
* @param quaternion
|
||||
*/
|
||||
flyTo(quaternion) {
|
||||
const focusPoint = new THREE.Vector3();
|
||||
const targetPosition = new THREE.Vector3(0, 0, 1);
|
||||
this.radius = this.camera.position.distanceTo(focusPoint);
|
||||
targetPosition.applyQuaternion(quaternion).multiplyScalar(this.radius).add(focusPoint);
|
||||
const dummy = new THREE.Object3D();
|
||||
dummy.position.copy(focusPoint);
|
||||
dummy.lookAt(this.camera.position);
|
||||
this.q1.copy(dummy.quaternion);
|
||||
dummy.lookAt(targetPosition);
|
||||
this.q2.copy(dummy.quaternion);
|
||||
this.animating = true;
|
||||
this.clock.start();
|
||||
}
|
||||
}
|
||||
export {
|
||||
AxesGizmo,
|
||||
DEFAULT_AXES_OPTIONS,
|
||||
DEFAULT_FACENAMES,
|
||||
DEFAULT_VIEWCUBE_OPTIONS,
|
||||
FixedPosGizmo,
|
||||
ObjectPosition,
|
||||
SimpleCameraControls,
|
||||
ViewCube,
|
||||
ViewCubeGizmo
|
||||
};
|
||||
@ -14,7 +14,14 @@ const strToHTML = (str) => str
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll(" ", " ");
|
||||
|
||||
export default function(render = createRender(qs(document.body, "[role=\"main\"]"))) {
|
||||
export default function(render) {
|
||||
let hasBack = true;
|
||||
if (!render) {
|
||||
render = createRender(document.body);
|
||||
try { render = createRender(qs(document.body, "[role=\"main\"]")); }
|
||||
catch (err) { hasBack = false; }
|
||||
}
|
||||
|
||||
return function(err) {
|
||||
const [msg, trace] = processError(err);
|
||||
|
||||
@ -22,7 +29,7 @@ export default function(render = createRender(qs(document.body, "[role=\"main\"]
|
||||
const $page = createElement(`
|
||||
<div>
|
||||
<style>${css}</style>
|
||||
<a href="${link}" class="backnav">
|
||||
<a href="${link}" class="backnav ${!hasBack && "hidden"}">
|
||||
<component-icon name="arrow_left"></component-icon>
|
||||
${t("home")}
|
||||
</a>
|
||||
|
||||
@ -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()),
|
||||
));
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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(`
|
||||
<div class="component_3dviewer">
|
||||
<component-menubar></component-menubar>
|
||||
<div class="threeviewer_container"></div>
|
||||
<component-menubar filename="${getFilename() || ""}" class="${!hasMenubar && "hidden"}"></component-menubar>
|
||||
<div class="threeviewer_container">
|
||||
<div class="drawarea"></div>
|
||||
<div class="toolbar scroll-y"></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
137
public/assets/pages/viewerpage/application_3d/init.js
Normal file
137
public/assets/pages/viewerpage/application_3d/init.js
Normal file
@ -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(`<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];
|
||||
}
|
||||
}
|
||||
23
public/assets/pages/viewerpage/application_3d/scene_cube.js
Normal file
23
public/assets/pages/viewerpage/application_3d/scene_cube.js
Normal file
@ -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));
|
||||
}
|
||||
38
public/assets/pages/viewerpage/application_3d/scene_light.js
Normal file
38
public/assets/pages/viewerpage/application_3d/scene_light.js
Normal file
@ -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;
|
||||
}
|
||||
97
public/assets/pages/viewerpage/application_3d/toolbar.js
Normal file
97
public/assets/pages/viewerpage/application_3d/toolbar.js
Normal file
@ -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(`<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNmMmYyZjIiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJtIDEuODI0MDExMiw2LjUzMDQxMTMgOS44OTUxMjU4LDUuMzc3MjgwNyBhIDAuNTkyMzE0MDQsMC41OTIzMTQwNCAwIDAgMCAwLjU1NzQ3NCwwIGwgOS44OTUxMywtNS4zNzcyODA3IGEgMC41MTEwMTYwMiwwLjUxMTAxNjAyIDAgMCAwIC0wLjA1ODA4LC0wLjk0MDczMzkgbCAtOS44OTUxMiwtNC4wNDE2NzI1IGEgMC41ODA3MDAwNSwwLjU4MDcwMDA1IDAgMCAwIC0wLjQ0MTMzNCwwIEwgMS44ODIwODI1LDUuNTg5Njc3NCBhIDAuNTExMDE2MDIsMC41MTEwMTYwMiAwIDAgMCAtMC4wNTgwNzEsMC45NDA3MzM5IHoiIC8+CiAgPHBhdGggZD0iTSAyMi4xMTM2NywxMC40NDQzMzIgMTkuOTg4MzA2LDkuNTM4NDM3NiAxMi4yNzY2MTEsMTMuNzMxMDkxIGEgMC41OTIzMTQwNCwwLjU5MjMxNDA0IDAgMCAxIC0wLjU1NzQ3NCwwIEwgNC4wMDc0NDM1LDkuNTM4NDM3NiAxLjg4MjA4MTIsMTAuNDQ0MzMyIGEgMC41NTc0NzIwNCwwLjU1NzQ3MjA0IDAgMCAwIDAsMC45ODcxODcgbCA5Ljg5NTEyOTgsNS42OTA4NTkgYSAwLjUzNDI0NDA3LDAuNTM0MjQ0MDcgMCAwIDAgMC41NTc0NzEsMCBsIDkuODk1MTMsLTUuNjkwODU5IEEgMC41NTc0NzIwNCwwLjU1NzQ3MjA0IDAgMCAwIDIyLjExMzY3LDEwLjQ0NDMzMiBaIiAvPgogIDxwYXRoIGQ9Im0gMjIuMTEzNjcsMTUuNjAwOTQzIC0xLjgxMTc4NCwtMC43ODk3NSAtOC4wMjUyNzUsNC4zNjY4NjYgYSAwLjU5MjMxNDA0LDAuNTkyMzE0MDQgMCAwIDEgLTAuNTU3NDc0LDAgbCAtOC4wMjUyNzE1LC00LjM2Njg2NiAtMS44MTE3ODQzLDAuNzg5NzUgYSAwLjU2OTA4NjAzLDAuNTY5MDg2MDMgMCAwIDAgMCwxLjAxMDQyMiBsIDkuODk1MTI5OCw1LjgwNjk5OCBhIDAuNTkyMzE0MDQsMC41OTIzMTQwNCAwIDAgMCAwLjU1NzQ3MSwwIGwgOS44OTUxMywtNS44MDY5OTggQSAwLjU2OTA4NjAzLDAuNTY5MDg2MDMgMCAwIDAgMjIuMTEzNjcsMTUuNjAwOTQzIFoiIC8+Cjwvc3ZnPgo=" alt="layers">`);
|
||||
$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<mesh.children.length; i++) {
|
||||
createChild($fragment, mesh.children[i], child + 1, opts);
|
||||
}
|
||||
}
|
||||
return $fragment;
|
||||
}
|
||||
|
||||
function buildDOM($fragment, child, left, { camera, controls }) {
|
||||
const $label = createElement(`
|
||||
<label class="no-select" style="padding-left: ${left*20}px">
|
||||
<div class="component_checkbox">
|
||||
<input type="checkbox" ${child.visible ? "checked" : ""} />
|
||||
<span class="indicator"></span>
|
||||
</div>
|
||||
<span class="text">${name(child)}</span>
|
||||
</label>
|
||||
`);
|
||||
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";
|
||||
}
|
||||
@ -7,7 +7,7 @@ export function transition($node) {
|
||||
}
|
||||
|
||||
export function getFilename() {
|
||||
return basename(getCurrentPath()) || "untitled.dat";
|
||||
return basename(getCurrentPath()) || " ";
|
||||
}
|
||||
|
||||
export function getDownloadUrl() {
|
||||
|
||||
@ -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 = `
|
||||
<div class="container">
|
||||
<span>
|
||||
<div class="titlebar ellipsis" style="opacity:0">${getFilename()}</div>
|
||||
<div class="titlebar ellipsis" style="opacity:0">${this.getAttribute("filename") || " "}</div>
|
||||
<div class="action-item no-select"></div>
|
||||
</span>
|
||||
</div>
|
||||
@ -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() {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user