mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-27 19:53:41 +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);
|
$anchor.appendChild($list);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await cache().remove("/", false);
|
await cache().remove("/", false);
|
||||||
if (nRestart < 2) ctrlSidebar(render, nRestart + 1);
|
if (err instanceof DOMException) return;
|
||||||
else if (err instanceof DOMException) {}
|
else if (nRestart < 2) ctrlSidebar(render, nRestart + 1);
|
||||||
else throw err;
|
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 rxjs, { ajax } from "./rx.js";
|
||||||
import { AjaxError } from "./error.js";
|
import { AjaxError } from "./error.js";
|
||||||
|
import { isSDK, urlSDK } from "../helpers/sdk.js";
|
||||||
|
|
||||||
export default function(opts) {
|
export default function(opts) {
|
||||||
if (typeof opts === "string") opts = { url: opts, withCredentials: true };
|
if (typeof opts === "string") opts = { url: opts, withCredentials: true };
|
||||||
@ -7,6 +8,12 @@ export default function(opts) {
|
|||||||
if (!opts.headers) opts.headers = {};
|
if (!opts.headers) opts.headers = {};
|
||||||
opts.headers["X-Requested-With"] = "XmlHttpRequest";
|
opts.headers["X-Requested-With"] = "XmlHttpRequest";
|
||||||
if (window.BEARER_TOKEN) opts.headers["Authorization"] = `Bearer ${window.BEARER_TOKEN}`;
|
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(
|
return ajax({ withCredentials: true, ...opts, responseType: "text" }).pipe(
|
||||||
rxjs.map((res) => {
|
rxjs.map((res) => {
|
||||||
const result = res.xhr.responseText;
|
const result = res.xhr.responseText;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
const settings = JSON.parse(window.localStorage.getItem("settings") || "null") || {};
|
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) {
|
if (settings[key] === undefined) {
|
||||||
return null;
|
return def;
|
||||||
}
|
}
|
||||||
return settings[key];
|
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(">", ">")
|
||||||
.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) {
|
return function(err) {
|
||||||
const [msg, trace] = processError(err);
|
const [msg, trace] = processError(err);
|
||||||
|
|
||||||
@ -22,7 +29,7 @@ export default function(render = createRender(qs(document.body, "[role=\"main\"]
|
|||||||
const $page = createElement(`
|
const $page = createElement(`
|
||||||
<div>
|
<div>
|
||||||
<style>${css}</style>
|
<style>${css}</style>
|
||||||
<a href="${link}" class="backnav">
|
<a href="${link}" class="backnav ${!hasBack && "hidden"}">
|
||||||
<component-icon name="arrow_left"></component-icon>
|
<component-icon name="arrow_left"></component-icon>
|
||||||
${t("home")}
|
${t("home")}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import { init as initMenubar } from "./viewerpage/component_menubar.js";
|
|||||||
import { init as initCache } from "./filespage/cache.js";
|
import { init as initCache } from "./filespage/cache.js";
|
||||||
|
|
||||||
import ctrlError from "./ctrl_error.js";
|
import ctrlError from "./ctrl_error.js";
|
||||||
|
import { getFilename, getDownloadUrl, getCurrentPath } from "./viewerpage/common.js";
|
||||||
import { opener } from "./viewerpage/mimetype.js";
|
import { opener } from "./viewerpage/mimetype.js";
|
||||||
import { getCurrentPath } from "./viewerpage/common.js";
|
|
||||||
import { options } from "./viewerpage/model_files.js";
|
import { options } from "./viewerpage/model_files.js";
|
||||||
|
|
||||||
import "../components/breadcrumb.js";
|
import "../components/breadcrumb.js";
|
||||||
@ -54,7 +54,7 @@ export default WithShell(async function(render) {
|
|||||||
effect(rxjs.of(window.CONFIG["mime"] || {}).pipe(
|
effect(rxjs.of(window.CONFIG["mime"] || {}).pipe(
|
||||||
rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)),
|
rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)),
|
||||||
rxjs.mergeMap(([opener, opts]) => rxjs.from(loadModule(opener)).pipe(rxjs.tap((module) => {
|
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()),
|
rxjs.catchError(ctrlError()),
|
||||||
));
|
));
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { createElement, createRender, createFragment, onDestroy } from "../../lib/skeleton/index.js";
|
import { createElement, createRender, createFragment, onDestroy } from "../../lib/skeleton/index.js";
|
||||||
import rxjs, { effect, onClick, preventDefault } from "../../lib/rx.js";
|
import rxjs, { effect, onClick, preventDefault } from "../../lib/rx.js";
|
||||||
import { animate, slideXIn, slideYIn } from "../../lib/animate.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 { loadCSS } from "../../helpers/loader.js";
|
||||||
import assert from "../../lib/assert.js";
|
import assert from "../../lib/assert.js";
|
||||||
import { qs, qsa } from "../../lib/dom.js";
|
import { qs, qsa } from "../../lib/dom.js";
|
||||||
import { basename } from "../../lib/path.js";
|
|
||||||
import t from "../../locales/index.js";
|
import t from "../../locales/index.js";
|
||||||
|
|
||||||
import "../../components/dropdown.js";
|
import "../../components/dropdown.js";
|
||||||
|
|||||||
@ -1,3 +1,47 @@
|
|||||||
.component_3dviewer {
|
.component_3dviewer .threeviewer_container {
|
||||||
background: #525659;
|
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 rxjs, { effect } from "../../lib/rx.js";
|
||||||
import { qs } from "../../lib/dom.js";
|
import { qs } from "../../lib/dom.js";
|
||||||
import { loadCSS } from "../../helpers/loader.js";
|
import { loadCSS } from "../../helpers/loader.js";
|
||||||
import { join } from "../../lib/path.js";
|
|
||||||
import { createLoader } from "../../components/loader.js";
|
import { createLoader } from "../../components/loader.js";
|
||||||
import { getFilename, getDownloadUrl } from "./common.js";
|
import ctrlError from "../ctrl_error.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 componentDownloader, { init as initDownloader } from "./application_downloader.js";
|
import componentDownloader, { init as initDownloader } from "./application_downloader.js";
|
||||||
|
|
||||||
import { renderMenubar, buttonDownload } from "./component_menubar.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(`
|
const $page = createElement(`
|
||||||
<div class="component_3dviewer">
|
<div class="component_3dviewer">
|
||||||
<component-menubar></component-menubar>
|
<component-menubar filename="${getFilename() || ""}" class="${!hasMenubar && "hidden"}"></component-menubar>
|
||||||
<div class="threeviewer_container"></div>
|
<div class="threeviewer_container">
|
||||||
|
<div class="drawarea"></div>
|
||||||
|
<div class="toolbar scroll-y"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
render($page);
|
render($page);
|
||||||
renderMenubar(qs($page, "component-menubar"), buttonDownload(getFilename(), getDownloadUrl()));
|
|
||||||
|
|
||||||
const removeLoader = createLoader(qs($page, ".threeviewer_container"));
|
const $menubar = renderMenubar(
|
||||||
effect(rxjs.of(getLoader(mime)).pipe(
|
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]) => {
|
rxjs.mergeMap(([loader, createMesh]) => {
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
componentDownloader(render);
|
componentDownloader(render, { mime, acl$ });
|
||||||
return rxjs.EMPTY;
|
return rxjs.EMPTY;
|
||||||
}
|
}
|
||||||
return rxjs.of([loader, createMesh]);
|
return rxjs.of([loader, createMesh]);
|
||||||
@ -44,81 +48,46 @@ export default function(render, { mime }) {
|
|||||||
(err) => observer.error(err),
|
(err) => observer.error(err),
|
||||||
))),
|
))),
|
||||||
removeLoader,
|
removeLoader,
|
||||||
rxjs.mergeMap((mesh) => {
|
rxjs.mergeMap((mesh) => create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube })),
|
||||||
// setup the dom
|
rxjs.catchError(ctrlError()),
|
||||||
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);
|
|
||||||
}));
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLoader(mime) {
|
function create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube }) {
|
||||||
const identity = (s) => s;
|
const refresh = [];
|
||||||
switch (mime) {
|
const { renderer, camera, scene, controls, box } = setup3D({
|
||||||
case "application/object":
|
$page: $draw,
|
||||||
return [new OBJLoader(), identity];
|
mesh,
|
||||||
case "model/3dm":
|
refresh,
|
||||||
const loader = new Rhino3dmLoader();
|
$menubar,
|
||||||
loader.setLibraryPath(join(import.meta.url, "../../lib/vendor/three/rhino3dm/"));
|
});
|
||||||
return [loader, identity];
|
|
||||||
case "model/gtlt-binary":
|
withLight({ scene, box });
|
||||||
case "model/gltf+json":
|
if (hasCube) withCube({ camera, renderer, refresh, controls });
|
||||||
return [new GLTFLoader(), (gltf) => gltf.scene];
|
ctrlToolbar(createRender($toolbar), {
|
||||||
case "model/stl":
|
mesh,
|
||||||
return [new STLLoader(), (geometry) => new THREE.Mesh(
|
controls,
|
||||||
geometry,
|
camera,
|
||||||
new THREE.MeshPhongMaterial(),
|
refresh,
|
||||||
)];
|
$menubar,
|
||||||
case "application/fbx":
|
$toolbar,
|
||||||
return [new FBXLoader(), identity];
|
});
|
||||||
default:
|
|
||||||
return [null, null];
|
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([
|
return Promise.all([
|
||||||
loadCSS(import.meta.url, "./application_3d.css"),
|
loadCSS(import.meta.url, "./application_3d.css"),
|
||||||
initDownloader(),
|
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() {
|
export function getFilename() {
|
||||||
return basename(getCurrentPath()) || "untitled.dat";
|
return basename(getCurrentPath()) || " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDownloadUrl() {
|
export function getDownloadUrl() {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { createElement } from "../../lib/skeleton/index.js";
|
|||||||
import { qs } from "../../lib/dom.js";
|
import { qs } from "../../lib/dom.js";
|
||||||
import { animate, slideYIn } from "../../lib/animate.js";
|
import { animate, slideYIn } from "../../lib/animate.js";
|
||||||
import { loadCSS } from "../../helpers/loader.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 assert from "../../lib/assert.js";
|
||||||
|
|
||||||
import "../../components/dropdown.js";
|
import "../../components/dropdown.js";
|
||||||
@ -14,7 +14,7 @@ export default class ComponentMenubar extends HTMLElement {
|
|||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span>
|
<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>
|
<div class="action-item no-select"></div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -45,6 +45,12 @@ export default class ComponentMenubar extends HTMLElement {
|
|||||||
}
|
}
|
||||||
animate($item, { time: 250, keyframes: slideYIn(2) });
|
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) {
|
export function buttonDownload(name, link) {
|
||||||
@ -61,6 +67,7 @@ export function buttonDownload(name, link) {
|
|||||||
`);
|
`);
|
||||||
const $img = qs($el, "img");
|
const $img = qs($el, "img");
|
||||||
qs($el, "a").onclick = () => {
|
qs($el, "a").onclick = () => {
|
||||||
|
if (isSDK()) return;
|
||||||
document.cookie = "download=yes; path=/; max-age=120;";
|
document.cookie = "download=yes; path=/; max-age=120;";
|
||||||
$img.setAttribute("src", ICON.LOADING);
|
$img.setAttribute("src", ICON.LOADING);
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
@ -92,6 +99,7 @@ export function buttonFullscreen($screen) {
|
|||||||
export function renderMenubar($menubar, ...buttons) {
|
export function renderMenubar($menubar, ...buttons) {
|
||||||
assert.type($menubar, ComponentMenubar);
|
assert.type($menubar, ComponentMenubar);
|
||||||
$menubar.render(buttons.filter(($button) => $button));
|
$menubar.render(buttons.filter(($button) => $button));
|
||||||
|
return $menubar;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function init() {
|
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 {
|
func IndexHeaders(fn HandlerFunc) HandlerFunc {
|
||||||
return HandlerFunc(func(ctx *App, res http.ResponseWriter, req *http.Request) {
|
return HandlerFunc(func(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||||
header := res.Header()
|
header := res.Header()
|
||||||
|
|||||||
@ -90,15 +90,15 @@ func Build(a App) *mux.Router {
|
|||||||
|
|
||||||
// Application Resources
|
// Application Resources
|
||||||
middlewares = []Middleware{ApiHeaders, SecureHeaders, PluginInjector}
|
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")
|
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
|
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("/favicon.ico"), NewMiddlewareChain(LegacyStaticHandler("/assets/logo/"), middlewares, a)).Methods("GET")
|
||||||
r.HandleFunc(WithBase("/sw_cache.js"), NewMiddlewareChain(LegacyStaticHandler("/assets/worker/"), 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
|
} 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")
|
r.HandleFunc(WithBase("/favicon.ico"), NewMiddlewareChain(ServeFavicon, middlewares, a)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user