mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-27 11:35:04 +08:00
feature (plugins): expand and migrate plugin - #803
* chore (dockerfile): cleanup dockerfile * feature (plugin): extend plugin interface * chore (docker): setup new Dockerfile * chore (dockerfile): update dockerfile
This commit is contained in:
@ -24,6 +24,7 @@
|
||||
"crw": "image/x-canon-crw",
|
||||
"css": "text/css",
|
||||
"csv": "text/csv",
|
||||
"dae": "model/vnd.collada+xml",
|
||||
"dbf": "application/dbf",
|
||||
"dcm": "image/dicom",
|
||||
"dcr": "image/x-kodak-dcr",
|
||||
@ -54,7 +55,8 @@
|
||||
"geojson": "application/geo+json",
|
||||
"gif": "image/gif",
|
||||
"gltf": "model/gltf+json",
|
||||
"glb": "model/gtlt-binary",
|
||||
"glb": "model/gltf-binary",
|
||||
"gpx": "application/gpx+xml",
|
||||
"gz": "application/x-gzip",
|
||||
"heic": "image/heic",
|
||||
"heif": "image/heic",
|
||||
|
||||
@ -1,74 +1,48 @@
|
||||
# STEP1: CLONE THE CODE
|
||||
FROM alpine:latest as builder_prepare
|
||||
FROM alpine/git as builder_prepare
|
||||
WORKDIR /home/
|
||||
ARG GIT_REPO=https://github.com/mickael-kerjean/filestash
|
||||
ARG GIT_REF=master
|
||||
RUN apk add --no-cache git && \
|
||||
git init filestash && \
|
||||
git -C filestash remote add origin ${GIT_REPO} && \
|
||||
git -C filestash fetch --depth 1 origin ${GIT_REF} && \
|
||||
git -C filestash checkout FETCH_HEAD
|
||||
ARG GIT_BRANCH=master
|
||||
RUN git clone --depth 1 --single-branch --branch ${GIT_BRANCH} ${GIT_REPO}
|
||||
|
||||
# STEP2: BUILD THE FRONTEND
|
||||
# STEP2: BUILD FRONTEND
|
||||
FROM node:18-alpine AS builder_frontend
|
||||
WORKDIR /home/
|
||||
COPY --from=builder_prepare /home/filestash/ ./
|
||||
WORKDIR /home/filestash/
|
||||
COPY --from=builder_prepare /home/filestash .
|
||||
RUN apk add make git gzip brotli && \
|
||||
npm install --legacy-peer-deps && \
|
||||
make build_frontend && \
|
||||
cd public && make compress
|
||||
|
||||
# STEP3: BUILD THE BACKEND
|
||||
# STEP3: BUILD BACKEND
|
||||
FROM golang:1.23-bookworm AS builder_backend
|
||||
WORKDIR /home/
|
||||
COPY --from=builder_frontend /home/ ./
|
||||
WORKDIR /home/filestash/
|
||||
COPY --from=builder_frontend /home/filestash/ .
|
||||
RUN apt-get update > /dev/null && \
|
||||
apt-get install -y libvips-dev curl make > /dev/null 2>&1 && \
|
||||
apt-get install -y libjpeg-dev libtiff-dev libpng-dev libwebp-dev libraw-dev libheif-dev libgif-dev && \
|
||||
apt-get install -y curl make > /dev/null 2>&1 && \
|
||||
apt-get install -y libjpeg-dev libtiff-dev libpng-dev libwebp-dev libraw-dev libheif-dev libgif-dev libvips-dev > /dev/null 2>&1 && \
|
||||
make build_init && \
|
||||
make build_backend && \
|
||||
mkdir -p ./dist/data/state/config/ && \
|
||||
cp config/config.json ./dist/data/state/config/config.json
|
||||
|
||||
# STEP4: Create the prod image from the build
|
||||
# STEP4: BUILD PLUGINS
|
||||
FROM emscripten/emsdk AS builder_final
|
||||
WORKDIR /home/filestash/
|
||||
COPY --from=builder_backend /home/filestash/ .
|
||||
RUN mkdir -p /home/filestash/dist/data/state/plugins && \
|
||||
cd /home/filestash/server/plugin/plg_application_dev/ && make && \
|
||||
cd /home/filestash/server/plugin/plg_application_3d/ && make
|
||||
|
||||
# STEP5: BUILD PROD IMAGE
|
||||
FROM debian:stable-slim
|
||||
MAINTAINER mickael@kerjean.me
|
||||
COPY --from=builder_backend /home/dist/ /app/
|
||||
WORKDIR "/app"
|
||||
WORKDIR /app/
|
||||
COPY --from=builder_final /home/filestash/dist/ .
|
||||
RUN apt-get update > /dev/null && \
|
||||
apt-get install -y --no-install-recommends apt-utils && \
|
||||
apt-get install -y curl emacs-nox ffmpeg zip poppler-utils > /dev/null && \
|
||||
# org-mode: html export
|
||||
curl https://raw.githubusercontent.com/mickael-kerjean/filestash/master/server/.assets/emacs/htmlize.el > /usr/share/emacs/site-lisp/htmlize.el && \
|
||||
# org-mode: markdown export
|
||||
curl https://raw.githubusercontent.com/mickael-kerjean/filestash/master/server/.assets/emacs/ox-gfm.el > /usr/share/emacs/site-lisp/ox-gfm.el && \
|
||||
# org-mode: pdf export (with a light latex distribution)
|
||||
cd && apt-get install -y wget perl > /dev/null && \
|
||||
export CTAN_REPO="http://mirror.las.iastate.edu/tex-archive/systems/texlive/tlnet" && \
|
||||
curl -sL "https://yihui.name/gh/tinytex/tools/install-unx.sh" | sh && \
|
||||
mv ~/.TinyTeX /usr/share/tinytex && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install wasy && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install ulem && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install marvosym && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install wasysym && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install xcolor && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install listings && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install parskip && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install float && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install wrapfig && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install sectsty && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install capt-of && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install epstopdf-pkg && \
|
||||
/usr/share/tinytex/bin/$(uname -m)-linux/tlmgr install cm-super && \
|
||||
ln -s /usr/share/tinytex/bin/$(uname -m)-linux/pdflatex /usr/local/bin/pdflatex && \
|
||||
apt-get purge -y --auto-remove perl wget && \
|
||||
# Cleanup
|
||||
find /usr/share/ -name 'doc' | xargs rm -rf && \
|
||||
find /usr/share/emacs -name '*.pbm' | xargs rm -f && \
|
||||
find /usr/share/emacs -name '*.png' | xargs rm -f && \
|
||||
find /usr/share/emacs -name '*.xpm' | xargs rm -f
|
||||
|
||||
RUN useradd filestash && \
|
||||
apt-get install -y curl ffmpeg libjpeg-dev libtiff-dev libpng-dev libwebp-dev libraw-dev libheif-dev libgif-dev && \
|
||||
useradd filestash && \
|
||||
chown -R filestash:filestash /app/ && \
|
||||
find /app/data/ -type d -exec chmod 770 {} \; && \
|
||||
find /app/data/ -type f -exec chmod 760 {} \; && \
|
||||
|
||||
@ -2,28 +2,27 @@ import { toHref } from "../lib/skeleton/router.js";
|
||||
import { loadJS } from "../helpers/loader.js";
|
||||
import { init as setup_translation } from "../locales/index.js";
|
||||
import { init as setup_config } from "../model/config.js";
|
||||
import { init as setup_plugin } from "../model/plugin.js";
|
||||
import { init as setup_chromecast } from "../model/chromecast.js";
|
||||
import { report } from "../helpers/log.js";
|
||||
import { $error } from "./common.js";
|
||||
|
||||
export default async function main() {
|
||||
try {
|
||||
await Promise.all([ // procedure with no outside dependencies
|
||||
setup_config(),
|
||||
await Promise.all([
|
||||
setup_config().then(() => Promise.all([
|
||||
setup_chromecast(),
|
||||
setup_title(),
|
||||
verify_origin(),
|
||||
])),
|
||||
setup_translation(),
|
||||
setup_xdg_open(),
|
||||
setup_device(),
|
||||
setup_blue_death_screen(),
|
||||
setup_history(),
|
||||
setup_polyfill(),
|
||||
setup_plugin(),
|
||||
]);
|
||||
|
||||
await Promise.all([ // procedure with dependency on config
|
||||
setup_chromecast(),
|
||||
setup_title(),
|
||||
verify_origin(),
|
||||
]);
|
||||
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export default async function(baseURL, path) {
|
||||
export default async function(baseURL, path, opts = {}) {
|
||||
const wasi = new Wasi();
|
||||
const wasm = await WebAssembly.instantiateStreaming(
|
||||
fetch(new URL(path, baseURL)), {
|
||||
@ -8,6 +8,7 @@ export default async function(baseURL, path) {
|
||||
env: {
|
||||
...wasi,
|
||||
...syscalls,
|
||||
...javascripts,
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -17,28 +18,40 @@ export default async function(baseURL, path) {
|
||||
|
||||
const FS = {};
|
||||
let nextFd = 0;
|
||||
writeFS(new Uint8Array(), 0); // stdin
|
||||
writeFS(new Uint8Array(), 1); // stdout
|
||||
writeFS(new Uint8Array(), 2); // stderr
|
||||
writeFS(new Uint8Array(0), "/dev/stdin");
|
||||
writeFS(new Uint8Array(1024*8), "/dev/stdout");
|
||||
writeFS(new Uint8Array(1024*8), "/dev/stderr");
|
||||
if (nextFd !== 3) throw new Error("Unexpected next fd");
|
||||
|
||||
export function writeFS(buffer, fd) {
|
||||
if (fd === undefined) fd = nextFd;
|
||||
else if (!(buffer instanceof Uint8Array)) throw new Error("can only write Uint8Array");
|
||||
|
||||
FS[fd] = {
|
||||
export function writeFS(buffer, path = "") {
|
||||
if (!(buffer instanceof Uint8Array)) throw new Error("can only write Uint8Array");
|
||||
FS[nextFd] = {
|
||||
buffer,
|
||||
position: 0,
|
||||
path,
|
||||
};
|
||||
nextFd += 1;
|
||||
return nextFd - 1;
|
||||
}
|
||||
|
||||
export function readFS(fd) {
|
||||
if (fd < 3) throw new Error("cannot read from stdin, stdout or stderr");
|
||||
const file = FS[fd];
|
||||
if (!file) throw new Error("file does not exist");
|
||||
return file.buffer;
|
||||
|
||||
let end = file.buffer.length;
|
||||
while (end > 0 && file.buffer[end - 1] === 0) end--;
|
||||
return file.buffer.subarray(0, end);
|
||||
}
|
||||
|
||||
function getFile(path) {
|
||||
const allFds = Object.keys(FS);
|
||||
for (let i=allFds.length - 1; i>0; i--) {
|
||||
if (FS[allFds[i]].path === path) {
|
||||
console.log(`fileopen fd=${i} path=${path}`);
|
||||
return FS[allFds[i]];
|
||||
}
|
||||
}
|
||||
throw new Error("cannot get file");
|
||||
}
|
||||
|
||||
export const syscalls = {
|
||||
@ -55,6 +68,64 @@ export const syscalls = {
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
__syscall_unlinkat: (fd) => {
|
||||
console.log(`Stubbed __syscall_unlinkat called with fd=${fd}`);
|
||||
return -1;
|
||||
},
|
||||
__syscall_rmdir: (fd) => {
|
||||
console.log(`Stubbed __syscall_rmdir called with fd=${fd}`);
|
||||
return -1;
|
||||
},
|
||||
__syscall_fstat64: (pathPtr, bufPtr) => {
|
||||
console.log(`Stubbed __syscall_stat64 called with pathPtr=${pathPtr}, bufPtr=${bufPtr}`);
|
||||
return 0; // Return 0 for a successful call
|
||||
},
|
||||
__syscall_newfstatat: (pathPtr, bufPtr) => {
|
||||
console.log(`Stubbed __syscall_stat64 called with pathPtr=${pathPtr}, bufPtr=${bufPtr}`);
|
||||
return 0; // Return 0 for a successful call
|
||||
},
|
||||
__syscall_lstat64: () => {
|
||||
console.log(`Stubbed __syscall_lstat64 called`);
|
||||
return -1;
|
||||
},
|
||||
__assert_fail: () => {
|
||||
console.log(`Stubbed __assert_fail called`);
|
||||
return -1;
|
||||
},
|
||||
__syscall_ftruncate64: () => {
|
||||
console.log(`Stubbed __syscall_ftruncate64`);
|
||||
return -1;
|
||||
},
|
||||
__syscall_renameat: () => {
|
||||
console.log(`Stubbed __syscall_renameat`);
|
||||
return -1;
|
||||
},
|
||||
};
|
||||
|
||||
const javascripts = {
|
||||
_tzset_js: () => {
|
||||
console.log("Initializing time zone settings (stub)");
|
||||
},
|
||||
_abort_js: () => {
|
||||
console.error("WebAssembly module called _abort_js!");
|
||||
throw new Error("_abort_js was called");
|
||||
},
|
||||
_mktime_js: () => {
|
||||
console.error("WebAssembly module called _abort_js!");
|
||||
throw new Error("_abort_js was called");
|
||||
},
|
||||
_localtime_js: () => {
|
||||
console.error("WebAssembly module called _localtime_js!");
|
||||
throw new Error("_localtime_js was called");
|
||||
},
|
||||
emscripten_date_now: () => {
|
||||
console.error("WebAssembly module called emscripten_date_now!");
|
||||
throw new Error("_localtime_js was called");
|
||||
},
|
||||
emscripten_get_now: () => {
|
||||
console.error("WebAssembly module called emscripten_get_now!");
|
||||
throw new Error("_localtime_js was called");
|
||||
},
|
||||
};
|
||||
|
||||
export class Wasi {
|
||||
@ -65,6 +136,16 @@ export class Wasi {
|
||||
this.fd_write = this.fd_write.bind(this);
|
||||
this.fd_seek = this.fd_seek.bind(this);
|
||||
this.fd_close = this.fd_close.bind(this);
|
||||
|
||||
this._emscripten_memcpy_js = this._emscripten_memcpy_js.bind(this);
|
||||
this.emscripten_resize_heap = this.emscripten_resize_heap.bind(this);
|
||||
this.environ_sizes_get = this.environ_sizes_get.bind(this);
|
||||
this.environ_get = this.environ_get.bind(this);
|
||||
this.clock_time_get = this.clock_time_get.bind(this);
|
||||
this.__syscall_openat = this.__syscall_openat.bind(this);
|
||||
this.__syscall_stat64 = this.__syscall_stat64.bind(this);
|
||||
this.__cxa_throw = this.__cxa_throw.bind(this);
|
||||
this.random_get = this.random_get.bind(this);
|
||||
}
|
||||
|
||||
set instance(val) {
|
||||
@ -72,33 +153,36 @@ export class Wasi {
|
||||
}
|
||||
|
||||
fd_write(fd, iovs, iovs_len, nwritten) {
|
||||
if (!FS[fd]) {
|
||||
console.error(`Invalid fd: ${fd}`);
|
||||
return -1;
|
||||
}
|
||||
let output = FS[fd].buffer;
|
||||
if (!FS[fd]) throw new Error(`File descriptor ${fd} does not exist.`);
|
||||
|
||||
const ioVecArray = new Uint32Array(this.#instance.exports.memory.buffer, iovs, iovs_len * 2);
|
||||
const memory = new Uint8Array(this.#instance.exports.memory.buffer);
|
||||
let totalBytesWritten = 0;
|
||||
for (let i = 0; i < iovs_len * 2; i += 2) {
|
||||
const sub = memory.subarray(
|
||||
(ioVecArray[i] || 0),
|
||||
(ioVecArray[i] || 0) + (ioVecArray[i+1] || 0),
|
||||
);
|
||||
const tmp = new Uint8Array(output.byteLength + sub.byteLength);
|
||||
tmp.set(output, 0);
|
||||
tmp.set(sub, output.byteLength);
|
||||
output = tmp;
|
||||
totalBytesWritten += ioVecArray[i+1] || 0;
|
||||
}
|
||||
const dataView = new DataView(this.#instance.exports.memory.buffer);
|
||||
dataView.setUint32(nwritten, totalBytesWritten, true);
|
||||
|
||||
FS[fd].buffer = output;
|
||||
if (fd < 3 && fd >= 0) {
|
||||
const msg = fd === 1 ? "stdout" : fd === 2 ? "stderr" : "stdxx";
|
||||
console.log(msg + ": " + (new TextDecoder()).decode(output));
|
||||
FS[fd].buffer = new ArrayBuffer(0);
|
||||
for (let i = 0; i < iovs_len * 2; i += 2) {
|
||||
const offset = ioVecArray[i];
|
||||
const length = ioVecArray[i + 1];
|
||||
while (FS[fd].buffer.byteLength - FS[fd].position < length) {
|
||||
const newBuffer = new Uint8Array(FS[fd].buffer.byteLength + 1024 * 1024 * 5);
|
||||
newBuffer.set(FS[fd].buffer, 0);
|
||||
FS[fd].buffer = newBuffer;
|
||||
}
|
||||
FS[fd].buffer.set(
|
||||
memory.subarray(offset, offset + length),
|
||||
FS[fd].position
|
||||
);
|
||||
FS[fd].position += length;
|
||||
totalBytesWritten += length;
|
||||
}
|
||||
new DataView(this.#instance.exports.memory.buffer).setUint32(
|
||||
nwritten,
|
||||
totalBytesWritten,
|
||||
true,
|
||||
);
|
||||
if (fd === 1 || fd === 2) {
|
||||
let msg = fd === 1? "stdout: " : "stderr: ";
|
||||
msg += new TextDecoder().decode(readFS(fd));
|
||||
FS[fd] = { buffer: new Uint8Array(0), position: 0, path: "" };
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -130,9 +214,11 @@ export class Wasi {
|
||||
file.position += bytesToRead;
|
||||
totalBytesRead += bytesToRead;
|
||||
}
|
||||
|
||||
const dataView = new DataView(this.#instance.exports.memory.buffer);
|
||||
dataView.setUint32(nread, totalBytesRead, true);
|
||||
new DataView(this.#instance.exports.memory.buffer).setUint32(
|
||||
nread,
|
||||
totalBytesRead,
|
||||
true,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -169,4 +255,126 @@ export class Wasi {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
_emscripten_memcpy_js(dest, src, num) {
|
||||
const memory = new Uint8Array(this.#instance.exports.memory.buffer);
|
||||
memory.set(memory.subarray(src, src + num), dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
emscripten_resize_heap() {
|
||||
console.log("Stubbed emscripten_resize_heap called");
|
||||
throw new Error("Heap resize not supported");
|
||||
}
|
||||
|
||||
environ_sizes_get() {
|
||||
console.log(`Stubbed environ_sizes_get called`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
environ_get() {
|
||||
console.log(`Stubbed environ_get called`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
clock_time_get() {
|
||||
console.log(`Stubbed clock_time_get called`);
|
||||
return -1;
|
||||
}
|
||||
|
||||
__syscall_openat(dirFd, pathPtr, flags, mode) {
|
||||
console.debug(`openat called with dirFd=${dirFd}, pathPtr=${pathPtr}, flags=${flags}, mode=${mode}`);
|
||||
const memory = new Uint8Array(this.#instance.exports.memory.buffer);
|
||||
let path = "";
|
||||
for (let i = pathPtr; memory[i] !== 0; i++) {
|
||||
path += String.fromCharCode(memory[i]);
|
||||
}
|
||||
const allFds = Object.keys(FS);
|
||||
for (let i=allFds.length - 1; i>0; i--) {
|
||||
if (FS[allFds[i]].path === path) {
|
||||
console.log(`fileopen fd=${i} path=${path}`);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
__syscall_stat64(pathPtr, buf) {
|
||||
console.log(`stat64`);
|
||||
const memory = new Uint8Array(this.#instance.exports.memory.buffer);
|
||||
let path = "";
|
||||
for (let i = pathPtr; memory[i] !== 0; i++) {
|
||||
path += String.fromCharCode(memory[i]);
|
||||
}
|
||||
const file = getFile(path);
|
||||
const HEAP32 = new Int32Array(this.#instance.exports.memory.buffer);
|
||||
const HEAPU32 = new Uint32Array(this.#instance.exports.memory.buffer);
|
||||
|
||||
const tempI64 = [0, 0];
|
||||
const tempDouble = 0;
|
||||
|
||||
// Dummy stat data
|
||||
const stat = {
|
||||
dev: 1,
|
||||
ino: 42,
|
||||
mode: 0o100644,
|
||||
nlink: 1,
|
||||
uid: 1000,
|
||||
gid: 1000,
|
||||
rdev: 0,
|
||||
size: file.buffer.byteLength,
|
||||
blksize: 4096,
|
||||
blocks: 256,
|
||||
atime: new Date(),
|
||||
mtime: new Date(),
|
||||
ctime: new Date(),
|
||||
};
|
||||
// Fill the buffer
|
||||
HEAP32[(buf >> 2)] = stat.dev;
|
||||
HEAP32[((buf + 4) >> 2)] = stat.mode;
|
||||
HEAPU32[((buf + 8) >> 2)] = stat.nlink;
|
||||
HEAP32[((buf + 12) >> 2)] = stat.uid;
|
||||
HEAP32[((buf + 16) >> 2)] = stat.gid;
|
||||
HEAP32[((buf + 20) >> 2)] = stat.rdev;
|
||||
HEAP32[((buf + 24) >> 2)] = stat.size & 0xFFFFFFFF; // Lower 32 bits
|
||||
HEAP32[((buf + 28) >> 2)] = Math.floor(stat.size / 4294967296); // Upper 32 bits
|
||||
HEAP32[((buf + 32) >> 2)] = stat.blksize;
|
||||
HEAP32[((buf + 36) >> 2)] = stat.blocks;
|
||||
|
||||
// Write timestamps
|
||||
const atimeSeconds = Math.floor(stat.atime.getTime() / 1000);
|
||||
const atimeNanos = (stat.atime.getTime() % 1000) * 1e6;
|
||||
HEAP32[((buf + 40) >> 2)] = atimeSeconds;
|
||||
HEAP32[((buf + 44) >> 2)] = 0; // Upper 32 bits of atime
|
||||
HEAP32[((buf + 48) >> 2)] = atimeNanos;
|
||||
|
||||
const mtimeSeconds = Math.floor(stat.mtime.getTime() / 1000);
|
||||
const mtimeNanos = (stat.mtime.getTime() % 1000) * 1e6;
|
||||
HEAP32[((buf + 56) >> 2)] = mtimeSeconds;
|
||||
HEAP32[((buf + 60) >> 2)] = 0; // Upper 32 bits of mtime
|
||||
HEAP32[((buf + 64) >> 2)] = mtimeNanos;
|
||||
|
||||
const ctimeSeconds = Math.floor(stat.ctime.getTime() / 1000);
|
||||
const ctimeNanos = (stat.ctime.getTime() % 1000) * 1e6;
|
||||
HEAP32[((buf + 72) >> 2)] = ctimeSeconds;
|
||||
HEAP32[((buf + 76) >> 2)] = 0; // Upper 32 bits of ctime
|
||||
HEAP32[((buf + 80) >> 2)] = ctimeNanos;
|
||||
|
||||
// Dummy inode
|
||||
HEAP32[((buf + 88) >> 2)] = stat.ino & 0xFFFFFFFF; // Lower 32 bits
|
||||
HEAP32[((buf + 92) >> 2)] = Math.floor(stat.ino / 4294967296); // Upper 32 bits
|
||||
|
||||
console.debug(`Stubbed __syscall_stat64 called with pathPtr=${pathPtr}, bufPtr=${buf}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
__cxa_throw(ptr, type, destructor) {
|
||||
console.error(`Exception thrown at ptr=${ptr}, type=${type}, destructor=${destructor}`);
|
||||
throw new Error("WebAssembly exception");
|
||||
}
|
||||
|
||||
random_get() {
|
||||
console.log(`Stubbed random_get called`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
1772
public/assets/lib/vendor/three/3DMLoader.js
vendored
1772
public/assets/lib/vendor/three/3DMLoader.js
vendored
File diff suppressed because it is too large
Load Diff
2310
public/assets/lib/vendor/three/EXRLoader.js
vendored
2310
public/assets/lib/vendor/three/EXRLoader.js
vendored
File diff suppressed because it is too large
Load Diff
9
public/assets/lib/vendor/three/README.md
vendored
9
public/assets/lib/vendor/three/README.md
vendored
@ -1,9 +0,0 @@
|
||||
reference:
|
||||
- https://threejs.org/docs/index.html#examples/en/loaders/MTLLoader
|
||||
|
||||
example:
|
||||
- https://unpkg.com/three@0.160.0/build/three.module.js
|
||||
- https://unpkg.com/three@0.160.0/examples/jsm/controls/OrbitControls.js
|
||||
- https://unpkg.com/three@0.160.0/examples/jsm/loaders/GLTFLoader.js
|
||||
|
||||
*note*: update the file to get the right import location
|
||||
6961
public/assets/lib/vendor/three/rhino3dm/rhino3dm.d.ts
vendored
6961
public/assets/lib/vendor/three/rhino3dm/rhino3dm.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
28
public/assets/model/plugin.js
Normal file
28
public/assets/model/plugin.js
Normal file
@ -0,0 +1,28 @@
|
||||
import rxjs from "../lib/rx.js";
|
||||
import ajax from "../lib/ajax.js";
|
||||
|
||||
const plugin$ = ajax({
|
||||
url: "api/plugin",
|
||||
method: "GET",
|
||||
responseType: "json",
|
||||
}).pipe(
|
||||
rxjs.map(({ responseJSON }) => responseJSON.result),
|
||||
);
|
||||
|
||||
let plugins = {};
|
||||
|
||||
export async function init() {
|
||||
plugins = await plugin$.toPromise();
|
||||
}
|
||||
|
||||
export function get(mime) {
|
||||
return plugins[mime];
|
||||
}
|
||||
|
||||
export async function load(mime) {
|
||||
const specs = plugins[mime];
|
||||
if (!specs) return null;
|
||||
const [_, url] = specs;
|
||||
const module = await import(url);
|
||||
return module.default;
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { createElement, createRender, nop } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { effect } from "../../lib/rx.js";
|
||||
import { qs } from "../../lib/dom.js";
|
||||
import { load as loadPlugin } from "../../model/plugin.js";
|
||||
import { loadCSS } from "../../helpers/loader.js";
|
||||
import { createLoader } from "../../components/loader.js";
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
@ -8,11 +9,19 @@ import ctrlError from "../ctrl_error.js";
|
||||
import componentDownloader, { init as initDownloader } from "./application_downloader.js";
|
||||
import { renderMenubar, buttonDownload } from "./component_menubar.js";
|
||||
|
||||
import setup3D, { getLoader, is2D } from "./application_3d/init.js";
|
||||
import * as THREE from "../../../lib/vendor/three/three.module.js";
|
||||
import setup3D 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";
|
||||
|
||||
class I3DLoader {
|
||||
constructor() {}
|
||||
load() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
transform() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
is2D() { return false; }
|
||||
}
|
||||
|
||||
export default async function(render, { mime, acl$, getDownloadUrl = nop, getFilename = nop, hasCube = true, hasMenubar = true }) {
|
||||
const $page = createElement(`
|
||||
<div class="component_3dviewer">
|
||||
@ -33,38 +42,44 @@ export default async function(render, { mime, acl$, getDownloadUrl = nop, getFil
|
||||
const $toolbar = qs($page, ".toolbar");
|
||||
|
||||
const removeLoader = createLoader($draw);
|
||||
await effect(rxjs.of(getLoader(mime)).pipe(
|
||||
rxjs.mergeMap(([loader, createMesh]) => {
|
||||
await effect(rxjs.from(loadPlugin(mime)).pipe(
|
||||
rxjs.mergeMap(async (loader) => {
|
||||
if (!loader) {
|
||||
componentDownloader(render, { mime, acl$, getFilename, getDownloadUrl });
|
||||
return rxjs.EMPTY;
|
||||
}
|
||||
return rxjs.of([loader, createMesh]);
|
||||
return new (await loader(I3DLoader, { THREE }))();
|
||||
}),
|
||||
rxjs.mergeMap(([loader, createMesh]) => new rxjs.Observable((observer) => loader.load(
|
||||
rxjs.mergeMap((loader) => new rxjs.Observable((observer) => loader.load(
|
||||
getDownloadUrl(),
|
||||
(object) => observer.next(createMesh(object)),
|
||||
(object) => observer.next(loader.transform(object)),
|
||||
null,
|
||||
(err) => observer.error(err),
|
||||
))),
|
||||
)).pipe(
|
||||
removeLoader,
|
||||
rxjs.mergeMap((mesh) => create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube, mime })),
|
||||
rxjs.mergeMap((mesh) => create3DScene({
|
||||
mesh,
|
||||
$draw, $toolbar, $menubar,
|
||||
hasCube, mime, is2D: loader.is2D,
|
||||
})),
|
||||
)),
|
||||
rxjs.catchError(ctrlError()),
|
||||
));
|
||||
}
|
||||
|
||||
function create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube, mime }) {
|
||||
function create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube, is2D }) {
|
||||
const refresh = [];
|
||||
const { renderer, camera, scene, controls, box } = setup3D({
|
||||
THREE,
|
||||
$page: $draw,
|
||||
mesh,
|
||||
refresh,
|
||||
$menubar,
|
||||
mime,
|
||||
is2D,
|
||||
});
|
||||
|
||||
withLight({ scene, box });
|
||||
if (hasCube && !is2D(mime)) withCube({ camera, renderer, refresh, controls });
|
||||
if (hasCube && !is2D()) withCube({ camera, renderer, refresh, controls });
|
||||
ctrlToolbar(createRender($toolbar), {
|
||||
mesh,
|
||||
controls,
|
||||
@ -72,6 +87,7 @@ function create3DScene({ mesh, $draw, $toolbar, $menubar, hasCube, mime }) {
|
||||
refresh,
|
||||
$menubar,
|
||||
$toolbar,
|
||||
is2D,
|
||||
});
|
||||
|
||||
return rxjs.animationFrames().pipe(rxjs.tap(() => {
|
||||
|
||||
@ -1,18 +1,7 @@
|
||||
import { createElement, onDestroy } from "../../../lib/skeleton/index.js";
|
||||
import { join } from "../../../lib/path.js";
|
||||
import { OrbitControls } from "../../../../lib/vendor/three/OrbitControls.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 { SVGLoader } from "../../../lib/vendor/three/SVGLoader.js";
|
||||
import { Rhino3dmLoader } from "../../../lib/vendor/three/3DMLoader.js";
|
||||
|
||||
export default function({ $page, $menubar, mesh, refresh, mime }) {
|
||||
export default function({ THREE, $page, $menubar, mesh, refresh, is2D }) {
|
||||
// setup the dom
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, shadowMapEnabled: true });
|
||||
renderer.shadowMap.enabled = true;
|
||||
@ -39,7 +28,7 @@ export default function({ $page, $menubar, mesh, refresh, mime }) {
|
||||
);
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.zoomToCursor = true;
|
||||
if (is2D(mime)) {
|
||||
if (is2D()) {
|
||||
controls.enableRotate = false;
|
||||
controls.mouseButtons = {
|
||||
LEFT: THREE.MOUSE.PAN,
|
||||
@ -51,7 +40,7 @@ export default function({ $page, $menubar, mesh, refresh, mime }) {
|
||||
scene.add(mesh);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
camera.position.set(center.x, center.y, center.z + maxDim * (is2D(mime) ? 1.3 : 1.8));
|
||||
camera.position.set(center.x, center.y, center.z + maxDim * (is2D() ? 1.3 : 1.8));
|
||||
controls.target.copy(center);
|
||||
|
||||
const mixer = new THREE.AnimationMixer(mesh);
|
||||
@ -89,118 +78,3 @@ export default function({ $page, $menubar, mesh, refresh, mime }) {
|
||||
|
||||
return { renderer, scene, camera, controls, box };
|
||||
}
|
||||
|
||||
export function is2D(mime) {
|
||||
return ["image/svg+xml", "application/acad"].indexOf(mime) !== -1;
|
||||
}
|
||||
|
||||
export function getLoader(mime) {
|
||||
const identity = (s) => s;
|
||||
switch (mime) {
|
||||
case "application/object":
|
||||
return [
|
||||
new OBJLoader(),
|
||||
(obj) => {
|
||||
obj.name = "All";
|
||||
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 "image/svg+xml":
|
||||
const createMaterial = (color, opacity = 1) => new THREE.MeshBasicMaterial({
|
||||
color: new THREE.Color().setStyle(color),
|
||||
opacity,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
wireframe: false,
|
||||
});
|
||||
const threecolor = (color) => {
|
||||
if (color && color.substr && color.substr(0, 4) === "RGB(") {
|
||||
function componentToHex(c) {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? "0" + hex : hex;
|
||||
}
|
||||
const [r, g, b] = color.replace(/^RGB\(/, "").replace(/\)/, "").split(",").map((i) => parseInt(i));
|
||||
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
return [new SVGLoader(), (data) => {
|
||||
const group = new THREE.Group();
|
||||
group.name = "All";
|
||||
group.scale.y *= -1;
|
||||
let renderOrder = 0;
|
||||
for (const path of data.paths) {
|
||||
const fillColor = threecolor(path.userData.style.fill);
|
||||
if (fillColor !== undefined && fillColor !== "none") {
|
||||
const material = createMaterial(
|
||||
fillColor,
|
||||
path.userData.style.fillOpacity,
|
||||
);
|
||||
const shapes = SVGLoader.createShapes(path);
|
||||
for (const shape of shapes) {
|
||||
const mesh = new THREE.Mesh(
|
||||
new THREE.ShapeGeometry(shape),
|
||||
material,
|
||||
);
|
||||
mesh.renderOrder = renderOrder++;
|
||||
group.add(mesh);
|
||||
}
|
||||
}
|
||||
const strokeColor = threecolor(path.userData.style.stroke);
|
||||
if (strokeColor !== undefined && strokeColor !== "none") {
|
||||
const material = createMaterial(strokeColor);
|
||||
for (const subPath of path.subPaths) {
|
||||
const geometry = SVGLoader.pointsToStroke(subPath.getPoints(), path.userData.style);
|
||||
if (geometry) {
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.renderOrder = renderOrder++;
|
||||
group.add(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}];
|
||||
case "application/fbx":
|
||||
return [new FBXLoader(), (obj) => {
|
||||
obj.name = "All";
|
||||
return obj;
|
||||
}];
|
||||
default:
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { onDestroy } from "../../../lib/skeleton/index.js";
|
||||
import { ViewCubeGizmo, SimpleCameraControls, ObjectPosition } from "../../../lib/vendor/three/viewcube.js";
|
||||
import { ViewCubeGizmo, SimpleCameraControls, ObjectPosition } from "../../../../lib/vendor/three/viewcube.js";
|
||||
|
||||
export default function({ camera, renderer, refresh, controls }) {
|
||||
const viewCubeGizmo = new ViewCubeGizmo(camera, renderer, {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { settings_get } from "../../../lib/settings.js";
|
||||
import * as THREE from "../../../lib/vendor/three/three.module.js";
|
||||
import * as THREE from "../../../../lib/vendor/three/three.module.js";
|
||||
|
||||
const LIGHT_COLOR = 0xf5f5f5;
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { createElement } from "../../../lib/skeleton/index.js";
|
||||
import { qs } from "../../../lib/dom.js";
|
||||
import * as THREE from "../../../lib/vendor/three/three.module.js";
|
||||
import * as THREE from "../../../../lib/vendor/three/three.module.js";
|
||||
|
||||
export default function(render, { camera, controls, mesh, $menubar, $toolbar }) {
|
||||
export default function(render, { camera, controls, mesh, $menubar, $toolbar, is2D }) {
|
||||
if (mesh.children.length <= 1) return;
|
||||
|
||||
$menubar.add(buttonLayers({ $toolbar }));
|
||||
@ -10,7 +10,7 @@ export default function(render, { camera, controls, mesh, $menubar, $toolbar })
|
||||
document.createDocumentFragment(),
|
||||
mesh,
|
||||
0,
|
||||
{ camera, controls }
|
||||
{ camera, controls, is2D }
|
||||
));
|
||||
}
|
||||
|
||||
@ -25,13 +25,15 @@ function createChild($fragment, mesh, child = 0, opts) {
|
||||
buildDOM($fragment, mesh, child, opts);
|
||||
if (mesh.children.length > 0 && child < 4) {
|
||||
for (let i=0; i<mesh.children.length; i++) {
|
||||
if (mesh.children[i].type === "Group" || !!mesh.children[i].name) {
|
||||
createChild($fragment, mesh.children[i], child + 1, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fragment;
|
||||
}
|
||||
|
||||
function buildDOM($fragment, child, left, { camera, controls }) {
|
||||
function buildDOM($fragment, child, left, { camera, controls, is2D }) {
|
||||
const $label = createElement(`
|
||||
<label class="no-select" style="padding-left: ${left*20}px">
|
||||
<div class="component_checkbox">
|
||||
@ -43,7 +45,8 @@ function buildDOM($fragment, child, left, { camera, controls }) {
|
||||
`);
|
||||
qs($label, "input").onchange = () => child.visible = !child.visible;
|
||||
$label.onclick = async(e) => {
|
||||
if (e.target.nodeName === "INPUT" || e.target.classList.contains("component_checkbox")) return;
|
||||
if (is2D()) return;
|
||||
else if (e.target.nodeName === "INPUT" || e.target.classList.contains("component_checkbox")) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
getRootObject(child).traverse((c) => {
|
||||
if (!c.material) return;
|
||||
|
||||
@ -4,12 +4,20 @@ import { qs, qsa } from "../../lib/dom.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
import { loadCSS } from "../../helpers/loader.js";
|
||||
import t from "../../locales/index.js";
|
||||
import { get as getPlugin } from "../../model/plugin.js";
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
|
||||
import { renderMenubar, buttonDownload } from "./component_menubar.js";
|
||||
import { getLoader } from "./application_table/loader.js";
|
||||
import { transition } from "./common.js";
|
||||
|
||||
const MAX_ROWS = 200;
|
||||
|
||||
class ITable {
|
||||
contructor() {}
|
||||
getHeader() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
getBody() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
}
|
||||
|
||||
export default async function(render, { mime, getDownloadUrl = nop, getFilename = nop, hasMenubar = true }) {
|
||||
const $page = createElement(`
|
||||
<div class="component_tableviewer">
|
||||
@ -32,76 +40,51 @@ export default async function(render, { mime, getDownloadUrl = nop, getFilename
|
||||
tbody: qs($page, ".tbody"),
|
||||
};
|
||||
const padding = 10;
|
||||
const STATE = {
|
||||
header: {},
|
||||
body: [],
|
||||
rows: [],
|
||||
};
|
||||
|
||||
// feature: initial render
|
||||
const init$ = ajax({ url: getDownloadUrl(), responseType: "arraybuffer" }).pipe(
|
||||
rxjs.mergeMap(async({ response }) => {
|
||||
const table = new (await getLoader(mime))(response);
|
||||
const loader = getPlugin(mime);
|
||||
if (!loader) throw new TypeError(`unsupported mimetype "${mime}"`);
|
||||
const [_, url] = loader;
|
||||
const module = await import(url);
|
||||
const table = new (await module.default(ITable))(response, { $menubar });
|
||||
STATE.header = table.getHeader();
|
||||
STATE.body = table.getBody();
|
||||
STATE.rows = STATE.body;
|
||||
|
||||
// build head
|
||||
const $tr = createElement(`<div class="tr"></div>`);
|
||||
table.getHeader().forEach(({ name, size }) => {
|
||||
const $th = createElement(`
|
||||
<div title="${name}" class="${withCenter("th ellipsis", size)}" style="${styleCell(size, name, padding)}">
|
||||
${name}
|
||||
<img class="no-select" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMyMSIgZD0ibSA3LjcwNSw4LjA0NSA0LjU5LDQuNTggNC41OSwtNC41OCAxLjQxLDEuNDEgLTYsNiAtNiwtNiB6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" />
|
||||
</div>
|
||||
`);
|
||||
let ascending = null;
|
||||
qs($th, "img").onclick = (e) => {
|
||||
ascending = !ascending;
|
||||
sortBy(qsa($dom.tbody, `.tr [data-column="${name}"]`), ascending);
|
||||
qsa(e.target.closest(".tr"), "img").forEach(($img) => {
|
||||
$img.style.transform = "rotate(0deg)";
|
||||
});
|
||||
if (ascending) e.target.style.transform = "rotate(180deg)";
|
||||
};
|
||||
$tr.appendChild($th);
|
||||
});
|
||||
$dom.thead.appendChild($tr);
|
||||
|
||||
// build body
|
||||
const body = table.getBody();
|
||||
body.forEach((obj) => {
|
||||
const $tr = createElement(`<div class="tr"></div>`);
|
||||
table.getHeader().forEach(({ name, size }) => {
|
||||
$tr.appendChild(createElement(`
|
||||
<div data-column="${name}" title="${obj[name]}" class="${withCenter("td ellipsis", size)}" style="${styleCell(size, name, padding)}">
|
||||
${obj[name] || "<span class=\"empty\">-</span>"}
|
||||
</div>
|
||||
`));
|
||||
});
|
||||
$dom.tbody.appendChild($tr);
|
||||
});
|
||||
if (body.length === 0) $dom.tbody.appendChild(createElement(`
|
||||
<h3 class="center no-select" style="opacity:0.2; margin-top:30px">
|
||||
${t("Empty")}
|
||||
</h3>
|
||||
`));
|
||||
transition($dom.tbody.parentElement);
|
||||
buildHead(STATE, $dom, padding);
|
||||
buildRows(STATE.rows.slice(0, MAX_ROWS), STATE.header, $dom.tbody, padding, true, false);
|
||||
}),
|
||||
rxjs.share(),
|
||||
rxjs.catchError(ctrlError()),
|
||||
rxjs.share(),
|
||||
);
|
||||
effect(init$);
|
||||
|
||||
// feature: search
|
||||
const $search = createElement(`<input type="search" placeholder="search">`);
|
||||
$menubar.add($search);
|
||||
effect(rxjs.fromEvent($search, "keydown").pipe(
|
||||
rxjs.debounceTime(200),
|
||||
effect(init$.pipe(
|
||||
rxjs.tap(() => $menubar.add($search)),
|
||||
rxjs.mergeMap(() => rxjs.fromEvent($search, "keydown").pipe(rxjs.debounceTime(200))),
|
||||
rxjs.tap((e) => {
|
||||
const terms = e.target.value.toLowerCase().split(" ");
|
||||
qsa($page, ".table .tbody .tr").forEach(($row) => {
|
||||
const str = $row.innerText.toLowerCase();
|
||||
const terms = e.target.value.toLowerCase().trim().split(" ");
|
||||
$dom.tbody.scrollTo(0, 0);
|
||||
if (terms === "") STATE.rows = STATE.body;
|
||||
else STATE.rows = STATE.body.filter((row) => {
|
||||
const line = Object.values(row).join("").toLowerCase();
|
||||
for (let i=0; i<terms.length; i++) {
|
||||
if (str.indexOf(terms[i]) === -1) {
|
||||
$row.classList.add("hidden");
|
||||
return;
|
||||
if (line.indexOf(terms[i]) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$row.classList.remove("hidden");
|
||||
return true;
|
||||
});
|
||||
buildRows(STATE.rows.slice(0, MAX_ROWS), STATE.header, $dom.tbody, padding, false, true);
|
||||
}),
|
||||
));
|
||||
|
||||
@ -113,12 +96,26 @@ export default async function(render, { mime, getDownloadUrl = nop, getFilename
|
||||
rxjs.tap(() => $dom.tbody.scrollTo($dom.thead.scrollLeft, $dom.tbody.scrollTop))
|
||||
));
|
||||
|
||||
// feature: infinite scroll
|
||||
effect(rxjs.fromEvent($dom.tbody, "scroll").pipe(
|
||||
rxjs.mergeMap(async (e) => {
|
||||
const scrollBottom = e.target.scrollHeight - (e.target.scrollTop + e.target.clientHeight);
|
||||
if (scrollBottom > 0) return;
|
||||
else if (STATE.rows.length <= MAX_ROWS) return;
|
||||
else if (STATE.rows.length <= $dom.tbody.children.length) return;
|
||||
|
||||
const current = $dom.tbody.children.length;
|
||||
const newRows = STATE.rows.slice(current, current + 10);
|
||||
buildRows(newRows, STATE.header, $dom.tbody, padding, false, false);
|
||||
}),
|
||||
));
|
||||
|
||||
// feature: make the last column to always fit the viewport
|
||||
effect(rxjs.merge(
|
||||
init$,
|
||||
init$.pipe(
|
||||
rxjs.mergeMap(() => rxjs.fromEvent(window, "resize")),
|
||||
rxjs.debounceTime(100),
|
||||
rxjs.debounce((e) => e.debounce === false ? rxjs.of(null) : rxjs.timer(100)),
|
||||
),
|
||||
).pipe(
|
||||
rxjs.tap(() => resizeLastColumnIfNeeded({
|
||||
@ -140,6 +137,61 @@ export function init() {
|
||||
]);
|
||||
}
|
||||
|
||||
async function buildRows(rows, legends, $tbody, padding, isInit, withClear) {
|
||||
if (withClear) $tbody.innerHTML = "";
|
||||
for (let i=0; i<rows.length; i++) {
|
||||
const obj = rows[i];
|
||||
const $tr = createElement(`<div class="tr"></div>`);
|
||||
legends.forEach(({ name, size }, i) => {
|
||||
$tr.appendChild(createElement(`
|
||||
<div data-column="${name}" title="${obj[name]}" class="${withCenter("td ellipsis", size, i === legends.length -1)}" style="${styleCell(size, name, padding)}">
|
||||
${obj[name] || "<span class=\"empty\">-</span>"}
|
||||
</div>
|
||||
`));
|
||||
});
|
||||
$tbody.appendChild($tr);
|
||||
}
|
||||
$tbody.style.opacity = "0";
|
||||
if (rows.length === 0) $tbody.appendChild(createElement(`
|
||||
<h3 class="center no-select" style="opacity:0.2; margin-top:30px">
|
||||
${t("Empty")}
|
||||
</h3>
|
||||
`));
|
||||
if (!isInit) {
|
||||
const e = new Event("resize");
|
||||
e.debounce = false;
|
||||
window.dispatchEvent(e);
|
||||
await new Promise(requestAnimationFrame);
|
||||
}
|
||||
$tbody.style.opacity = "1";
|
||||
if (isInit) transition($tbody.parentElement);
|
||||
}
|
||||
|
||||
function buildHead(STATE, $dom, padding) {
|
||||
const $tr = createElement(`<div class="tr"></div>`);
|
||||
STATE.header.forEach(({ name, size }, i) => {
|
||||
const $th = createElement(`
|
||||
<div title="${name}" class="${withCenter("th ellipsis", size, i === STATE.header.length - 1)}" style="${styleCell(size, name, padding)}">
|
||||
${name}
|
||||
<img class="no-select" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMyMSIgZD0ibSA3LjcwNSw4LjA0NSA0LjU5LDQuNTggNC41OSwtNC41OCAxLjQxLDEuNDEgLTYsNiAtNiwtNiB6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" />
|
||||
</div>
|
||||
`);
|
||||
let ascending = null;
|
||||
qs($th, "img").onclick = (e) => {
|
||||
ascending = !ascending;
|
||||
STATE.rows = sortBy(STATE.rows, ascending, name);
|
||||
qsa(e.target.closest(".tr"), "img").forEach(($img) => {
|
||||
$img.style.transform = "rotate(0deg)";
|
||||
});
|
||||
if (ascending) e.target.style.transform = "rotate(180deg)";
|
||||
$dom.tbody.scrollTo(0, 0);
|
||||
buildRows(STATE.rows.slice(0, MAX_ROWS), STATE.header, $dom.tbody, padding, false, true);
|
||||
};
|
||||
$tr.appendChild($th);
|
||||
});
|
||||
$dom.thead.appendChild($tr);
|
||||
}
|
||||
|
||||
function styleCell(l, name, padding) {
|
||||
const maxSize = 40;
|
||||
const charSize = 7;
|
||||
@ -148,8 +200,8 @@ function styleCell(l, name, padding) {
|
||||
return `width: ${sizeInChar*charSize+padding*2}px;`;
|
||||
}
|
||||
|
||||
function withCenter(className, fieldLength) {
|
||||
if (fieldLength > 4) return className;
|
||||
function withCenter(className, fieldLength, isLast) {
|
||||
if (fieldLength > 4 || isLast) return className;
|
||||
return `${className} center`;
|
||||
}
|
||||
|
||||
@ -157,20 +209,17 @@ function resizeLastColumnIfNeeded({ $target, $childs, padding = 0 }) {
|
||||
const fullWidth = $target.clientWidth;
|
||||
let currWidth = 0;
|
||||
$childs.childNodes.forEach(($node) => currWidth += $node.clientWidth);
|
||||
if (currWidth < fullWidth) {
|
||||
if (currWidth < fullWidth && $childs.lastChild !== null) {
|
||||
const lastWidth = ($childs.lastChild.clientWidth - padding * 2) + fullWidth - currWidth;
|
||||
$childs.lastChild.setAttribute("style", `width: ${lastWidth}px`);
|
||||
}
|
||||
}
|
||||
|
||||
function sortBy($columns, ascending) {
|
||||
function sortBy(rows, ascending, key) {
|
||||
const o = ascending ? 1 : -1;
|
||||
const $new = [...$columns].sort(($el1, $el2) => {
|
||||
if ($el1.innerText === $el2.innerText) return 0;
|
||||
else if ($el1.innerText < $el2.innerText) return -o;
|
||||
return rows.sort((a, b) => {
|
||||
if (a[key] === b[key]) return 0;
|
||||
else if (a[key] < b[key]) return -o
|
||||
return o;
|
||||
});
|
||||
const $root = $columns[0].parentElement.parentElement;
|
||||
$root.innerHTML = "";
|
||||
$new.forEach(($node) => $root.appendChild($node.parentElement));
|
||||
}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
all:
|
||||
emcc loader_symbol.c -o loader_symbol.wasm -O2 --no-entry
|
||||
@ -1,24 +0,0 @@
|
||||
// import loaderDBase from "./loader_dbase.js";
|
||||
// import loaderSymbol from "./loader_symbol.js";
|
||||
|
||||
class ITable {
|
||||
contructor() {}
|
||||
getHeader() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
getBody() { throw new Error("NOT_IMPLEMENTED"); }
|
||||
}
|
||||
|
||||
export async function getLoader(mime) {
|
||||
let module = null;
|
||||
switch (mime) {
|
||||
case "application/dbf":
|
||||
module = await import("./loader_dbase.js");
|
||||
break;
|
||||
case "application/x-archive":
|
||||
module = await import("./loader_symbol.js");
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`unsupported mimetype '${mime}'`);
|
||||
}
|
||||
|
||||
return module.default(ITable);
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
export default async function(ITable) {
|
||||
const module = await import("../../../lib/vendor/shp-to-geojson.browser.js");
|
||||
|
||||
return class TableImpl extends ITable {
|
||||
constructor(response) {
|
||||
super();
|
||||
this.data = new module.DBase(module.Buffer.from(response));
|
||||
}
|
||||
|
||||
getHeader() {
|
||||
return this.data.properties.map(({ fieldName, fieldLength }) => ({
|
||||
name: fieldName,
|
||||
size: fieldLength,
|
||||
}));
|
||||
}
|
||||
|
||||
getBody() {
|
||||
const body = [];
|
||||
for (let i =0; i<this.data.recordLength; i++) {
|
||||
const row = this.data.getRowProperties(i);
|
||||
body.push(row);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
};
|
||||
}
|
||||
Binary file not shown.
@ -1,3 +1,5 @@
|
||||
import { get as getPlugin } from "../../model/plugin.js";
|
||||
|
||||
export function opener(file = "", mimes) {
|
||||
const mime = getMimeType(file, mimes);
|
||||
const type = mime.split("/")[0];
|
||||
@ -9,12 +11,16 @@ export function opener(file = "", mimes) {
|
||||
}
|
||||
}
|
||||
|
||||
const p = getPlugin(mime);
|
||||
if (p) return [
|
||||
p[0],
|
||||
{ mime, loader: p[1] },
|
||||
];
|
||||
|
||||
if (type === "text") {
|
||||
return ["editor", { mime }];
|
||||
} else if (mime === "application/pdf") {
|
||||
return ["pdf", { mime }];
|
||||
} else if (type === "model" || ["image/svg+xml", "application/object", "application/fbx"].indexOf(mime) !== -1) {
|
||||
return ["3d", { mime }];
|
||||
} else if (type === "image") {
|
||||
return ["image", { mime }];
|
||||
} else if (["application/javascript", "application/xml", "application/json",
|
||||
@ -32,8 +38,6 @@ export function opener(file = "", mimes) {
|
||||
return ["ebook", { mime }];
|
||||
} else if (mime === "application/x-url") {
|
||||
return ["url", { mime }];
|
||||
} else if (["application/dbf", "application/x-archive"].indexOf(mime) !== -1) {
|
||||
return ["table", { mime }];
|
||||
} else if (type === "application" && mime !== "application/text") {
|
||||
return ["download", { mime }];
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ var (
|
||||
var (
|
||||
CONFIG_PATH = "state/config/"
|
||||
CERT_PATH = "state/certs/"
|
||||
PLUGIN_PATH = "state/plugins/"
|
||||
DB_PATH = "state/db/"
|
||||
FTS_PATH = "state/search/"
|
||||
LOG_PATH = "state/log/"
|
||||
@ -38,6 +39,7 @@ func init() {
|
||||
FTS_PATH = filepath.Join(rootPath, FTS_PATH)
|
||||
CERT_PATH = filepath.Join(rootPath, CERT_PATH)
|
||||
TMP_PATH = filepath.Join(rootPath, TMP_PATH)
|
||||
PLUGIN_PATH = filepath.Join(rootPath, PLUGIN_PATH)
|
||||
base = strings.TrimSuffix(base, "/")
|
||||
COOKIE_PATH_ADMIN = WithBase(COOKIE_PATH_ADMIN)
|
||||
COOKIE_PATH = WithBase(COOKIE_PATH)
|
||||
@ -48,6 +50,7 @@ func init() {
|
||||
os.MkdirAll(GetAbsolutePath(DB_PATH), os.ModePerm)
|
||||
os.MkdirAll(GetAbsolutePath(FTS_PATH), os.ModePerm)
|
||||
os.MkdirAll(GetAbsolutePath(LOG_PATH), os.ModePerm)
|
||||
os.MkdirAll(GetAbsolutePath(PLUGIN_PATH), os.ModePerm)
|
||||
os.RemoveAll(GetAbsolutePath(TMP_PATH))
|
||||
os.MkdirAll(GetAbsolutePath(TMP_PATH), os.ModePerm)
|
||||
}
|
||||
|
||||
57
server/ctrl/plugin.go
Normal file
57
server/ctrl/plugin.go
Normal file
@ -0,0 +1,57 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
"github.com/mickael-kerjean/filestash/server/model"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Hooks.Register.Onload(func() {
|
||||
if err := model.PluginDiscovery(); err != nil {
|
||||
Log.Error("Plugin Discovery failed. err=%s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func PluginExportHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
plgExports := map[string][]string{}
|
||||
for name, plg := range model.PLUGINS {
|
||||
for _, module := range plg.Modules {
|
||||
if module["type"] == "xdg-open" {
|
||||
index := module["entrypoint"]
|
||||
if index == "" {
|
||||
index = "/index.js"
|
||||
}
|
||||
plgExports[module["mime"]] = []string{
|
||||
module["application"],
|
||||
WithBase(JoinPath("/plugin/", name+index)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SendSuccessResult(res, plgExports)
|
||||
}
|
||||
|
||||
func PluginStaticHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
path := mux.Vars(req)["path"]
|
||||
mtype := GetMimeType(path)
|
||||
file, err := model.GetPluginFile(mux.Vars(req)["name"], path)
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
res.Header().Set("Content-Type", mtype)
|
||||
_, err = io.Copy(res, file)
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
115
server/model/plugin.go
Normal file
115
server/model/plugin.go
Normal file
@ -0,0 +1,115 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
)
|
||||
|
||||
var PLUGINS = map[string]PluginImpl{}
|
||||
|
||||
type PluginImpl struct {
|
||||
Author string `json:"author"`
|
||||
Version string `json:"version"`
|
||||
Modules []map[string]string `json:"modules"`
|
||||
}
|
||||
|
||||
func PluginDiscovery() error {
|
||||
f, err := os.Open(GetAbsolutePath(PLUGIN_PATH))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := f.ReadDir(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
fname := entry.Name()
|
||||
if strings.HasSuffix(fname, ".zip") == false {
|
||||
continue
|
||||
}
|
||||
name, modules, err := InitModule(entry.Name())
|
||||
if err != nil {
|
||||
Log.Error("could not initialise module name=%s err=%s", entry.Name(), err.Error())
|
||||
continue
|
||||
}
|
||||
PLUGINS[name] = modules
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type zrc struct {
|
||||
f io.ReadCloser
|
||||
c io.Closer
|
||||
}
|
||||
|
||||
func (this zrc) Read(p []byte) (n int, err error) {
|
||||
return this.f.Read(p)
|
||||
}
|
||||
|
||||
func (this zrc) Close() error {
|
||||
this.f.Close()
|
||||
this.c.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetPluginFile(pluginName string, path string) (io.ReadCloser, error) {
|
||||
zipReader, err := zip.OpenReader(JoinPath(
|
||||
GetAbsolutePath(PLUGIN_PATH),
|
||||
pluginName+".zip",
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, zipFile := range zipReader.File {
|
||||
if zipFile.Name != path {
|
||||
continue
|
||||
}
|
||||
f, err := zipFile.Open()
|
||||
if err != nil {
|
||||
zipReader.Close()
|
||||
return nil, err
|
||||
}
|
||||
return zrc{f, zipReader}, nil
|
||||
}
|
||||
zipReader.Close()
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func InitModule(plgName string) (string, PluginImpl, error) {
|
||||
var plgImpl = PluginImpl{}
|
||||
r, err := zip.OpenReader(JoinPath(GetAbsolutePath(PLUGIN_PATH), plgName))
|
||||
plgName = strings.TrimSuffix(plgName, ".zip")
|
||||
if err != nil {
|
||||
return plgName, plgImpl, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var manifestFile io.ReadCloser
|
||||
for _, f := range r.File {
|
||||
if f.Name != "manifest.json" {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return plgName, plgImpl, err
|
||||
}
|
||||
manifestFile = rc
|
||||
break
|
||||
}
|
||||
if manifestFile == nil {
|
||||
return plgName, plgImpl, ErrNotFound
|
||||
}
|
||||
defer manifestFile.Close()
|
||||
if err = json.NewDecoder(manifestFile).Decode(&plgImpl); err != nil {
|
||||
return plgName, plgImpl, err
|
||||
}
|
||||
return plgName, plgImpl, nil
|
||||
}
|
||||
6
server/plugin/plg_application_3d/Makefile
Normal file
6
server/plugin/plg_application_3d/Makefile
Normal file
@ -0,0 +1,6 @@
|
||||
all:
|
||||
make install
|
||||
|
||||
install:
|
||||
zip -r application_3d.zip .
|
||||
mv application_3d.zip ../../../dist/data/state/plugins/application_3d.zip
|
||||
18
server/plugin/plg_application_3d/index_fbx.js
Normal file
18
server/plugin/plg_application_3d/index_fbx.js
Normal file
@ -0,0 +1,18 @@
|
||||
export default async function(I3D, { THREE }) {
|
||||
const module = await import("./vendor/FBXLoader.js");
|
||||
|
||||
return class Impl extends I3D {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
load(url, onLoad, onProgress, onError) {
|
||||
return (new module.FBXLoader()).load(url, onLoad, onProgress, onError);
|
||||
}
|
||||
|
||||
transform(obj) {
|
||||
obj.name = "All";
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
server/plugin/plg_application_3d/index_gltf.js
Normal file
19
server/plugin/plg_application_3d/index_gltf.js
Normal file
@ -0,0 +1,19 @@
|
||||
export default async function(I3D, { THREE }) {
|
||||
const module = await import("./vendor/GLTFLoader.js");
|
||||
|
||||
return class Impl extends I3D {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
load(url, onLoad, onProgress, onError) {
|
||||
return new module.GLTFLoader().load(url, onLoad, onProgress, onError);
|
||||
}
|
||||
|
||||
transform(gltf) {
|
||||
const mesh = gltf.scene;
|
||||
mesh.animations = gltf.animations;
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
server/plugin/plg_application_3d/index_obj.js
Normal file
33
server/plugin/plg_application_3d/index_obj.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { toCreasedNormals } from "./vendor/utils/BufferGeometryUtils.js";
|
||||
|
||||
export default async function(I3D, { THREE }) {
|
||||
const module = await import("./vendor/OBJLoader.js");
|
||||
|
||||
return class Impl extends I3D {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
load(url, onLoad, onProgress, onError) {
|
||||
return (new module.OBJLoader()).load(url, onLoad, onProgress, onError);
|
||||
}
|
||||
|
||||
transform(obj) {
|
||||
obj.name = "All";
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
server/plugin/plg_application_3d/index_stl.js
Normal file
25
server/plugin/plg_application_3d/index_stl.js
Normal file
@ -0,0 +1,25 @@
|
||||
export default async function(I3D, { THREE }) {
|
||||
const module = await import("./vendor/STLLoader.js");
|
||||
|
||||
return class Impl extends I3D {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
load(url, onLoad, onProgress, onError) {
|
||||
return (new module.STLLoader()).load(url, onLoad, onProgress, onError);
|
||||
}
|
||||
|
||||
transform(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
server/plugin/plg_application_3d/index_svg.js
Normal file
76
server/plugin/plg_application_3d/index_svg.js
Normal file
@ -0,0 +1,76 @@
|
||||
export default async function(I3D, { THREE }) {
|
||||
const module = await import("./vendor/SVGLoader.js");
|
||||
|
||||
const threecolor = (color) => {
|
||||
if (color && color.substr && color.substr(0, 4) === "RGB(") {
|
||||
function componentToHex(c) {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? "0" + hex : hex;
|
||||
}
|
||||
const [r, g, b] = color.replace(/^RGB\(/, "").replace(/\)/, "").split(",").map((i) => parseInt(i));
|
||||
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
|
||||
const createMaterial = (color, opacity = 1) => new THREE.MeshBasicMaterial({
|
||||
color: new THREE.Color().setStyle(color),
|
||||
opacity,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
wireframe: false,
|
||||
});
|
||||
|
||||
return class Impl extends I3D {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
load(url, onLoad, onProgress, onError) {
|
||||
return (new module.SVGLoader()).load(url, onLoad, onProgress, onError);
|
||||
}
|
||||
|
||||
transform(data) {
|
||||
const group = new THREE.Group();
|
||||
group.name = "All";
|
||||
group.scale.y *= -1;
|
||||
let renderOrder = 0;
|
||||
for (const path of data.paths) {
|
||||
const fillColor = threecolor(path.userData.style.fill);
|
||||
if (fillColor !== undefined && fillColor !== "none") {
|
||||
const material = createMaterial(
|
||||
fillColor,
|
||||
path.userData.style.fillOpacity,
|
||||
);
|
||||
const shapes = module.SVGLoader.createShapes(path);
|
||||
for (const shape of shapes) {
|
||||
const mesh = new THREE.Mesh(
|
||||
new THREE.ShapeGeometry(shape),
|
||||
material,
|
||||
);
|
||||
mesh.renderOrder = renderOrder++;
|
||||
group.add(mesh);
|
||||
}
|
||||
}
|
||||
const strokeColor = threecolor(path.userData.style.stroke);
|
||||
if (strokeColor !== undefined && strokeColor !== "none") {
|
||||
const material = createMaterial(strokeColor);
|
||||
for (const subPath of path.subPaths) {
|
||||
const geometry = module.SVGLoader.pointsToStroke(subPath.getPoints(), path.userData.style);
|
||||
if (geometry) {
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.renderOrder = renderOrder++;
|
||||
group.add(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
is2D() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
server/plugin/plg_application_3d/manifest.json
Normal file
36
server/plugin/plg_application_3d/manifest.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"author": "Filestash Pty Ltd",
|
||||
"version": "v0.0",
|
||||
"modules": [
|
||||
{
|
||||
"type": "xdg-open",
|
||||
"mime": "application/fbx",
|
||||
"entrypoint": "/index_fbx.js",
|
||||
"application": "3d"
|
||||
},
|
||||
{
|
||||
"type": "xdg-open",
|
||||
"mime": "model/gltf-binary",
|
||||
"entrypoint": "/index_gltf.js",
|
||||
"application": "3d"
|
||||
},
|
||||
{
|
||||
"type": "xdg-open",
|
||||
"mime": "application/object",
|
||||
"entrypoint": "/index_obj.js",
|
||||
"application": "3d"
|
||||
},
|
||||
{
|
||||
"type": "xdg-open",
|
||||
"mime": "model/stl",
|
||||
"entrypoint": "/index_stl.js",
|
||||
"application": "3d"
|
||||
},
|
||||
{
|
||||
"type": "xdg-open",
|
||||
"mime": "image/svg+xml",
|
||||
"entrypoint": "/index_svg.js",
|
||||
"application": "3d"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -43,7 +43,7 @@ import {
|
||||
VectorKeyframeTrack,
|
||||
SRGBColorSpace,
|
||||
ShapeUtils
|
||||
} from './three.module.js';
|
||||
} from '../../../assets/lib/vendor/three/three.module.js';
|
||||
import * as fflate from './libs/fflate.module.js';
|
||||
import { NURBSCurve } from './curves/NURBSCurve.js';
|
||||
|
||||
@ -65,7 +65,7 @@ import {
|
||||
VectorKeyframeTrack,
|
||||
SRGBColorSpace,
|
||||
InstancedBufferAttribute
|
||||
} from './three.module.js';
|
||||
} from '../../../assets/lib/vendor/three/three.module.js';
|
||||
import { toTrianglesDrawMode } from './utils/BufferGeometryUtils.js';
|
||||
|
||||
class GLTFLoader extends Loader {
|
||||
@ -14,7 +14,7 @@ import {
|
||||
PointsMaterial,
|
||||
Vector3,
|
||||
Color
|
||||
} from './three.module.js';
|
||||
} from '../../../assets/lib/vendor/three/three.module.js';
|
||||
|
||||
// o object_name | g group_name
|
||||
const _object_pattern = /^[og]\s*(.+)?/;
|
||||
@ -7,7 +7,7 @@ import {
|
||||
Float32BufferAttribute,
|
||||
Loader,
|
||||
Vector3
|
||||
} from './three.module.js';
|
||||
} from '../../../assets/lib/vendor/three/three.module.js';
|
||||
|
||||
/**
|
||||
* Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
|
||||
@ -13,7 +13,7 @@ import {
|
||||
SRGBColorSpace,
|
||||
Vector2,
|
||||
Vector3
|
||||
} from './three.module.js';
|
||||
} from '../../../assets/lib/vendor/three/three.module.js';
|
||||
|
||||
const COLOR_SPACE_SVG = SRGBColorSpace;
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
Curve,
|
||||
Vector3,
|
||||
Vector4
|
||||
} from '../three.module.js';
|
||||
} from '../../../../assets/lib/vendor/three/three.module.js';
|
||||
import * as NURBSUtils from './NURBSUtils.js';
|
||||
|
||||
/**
|
||||
@ -2,7 +2,7 @@
|
||||
import {
|
||||
Vector3,
|
||||
Vector4
|
||||
} from '../three.module.js';
|
||||
} from '../../../../assets/lib/vendor/three/three.module.js';
|
||||
|
||||
/**
|
||||
* NURBS utils
|
||||
@ -10,7 +10,7 @@ import {
|
||||
TriangleStripDrawMode,
|
||||
TrianglesDrawMode,
|
||||
Vector3,
|
||||
} from '../three.module.js';
|
||||
} from '../../../../assets/lib/vendor/three/three.module.js';
|
||||
|
||||
function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
|
||||
|
||||
15
server/plugin/plg_application_dev/Makefile
Normal file
15
server/plugin/plg_application_dev/Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
all:
|
||||
make build
|
||||
make install
|
||||
make clean
|
||||
|
||||
build:
|
||||
emcc -O2 -c loader_symbol.c
|
||||
emcc --no-entry loader_symbol.o -o loader_symbol.wasm
|
||||
|
||||
install:
|
||||
zip -r application_dev.zip .
|
||||
mv application_dev.zip ../../../dist/data/state/plugins/
|
||||
|
||||
clean:
|
||||
rm *.o *.wasm
|
||||
@ -1,5 +1,5 @@
|
||||
import assert from "../../../lib/assert.js";
|
||||
import loadWASM, { writeFS, readFS } from "../../../helpers/loader_wasm.js";
|
||||
import assert from "../../assets/lib/assert.js";
|
||||
import loadWASM, { writeFS, readFS } from "../../assets/helpers/loader_wasm.js";
|
||||
|
||||
export default async function(ITable) {
|
||||
const { instance } = await loadWASM(import.meta.url, "./loader_symbol.wasm");
|
||||
@ -48,7 +48,7 @@ EMSCRIPTEN_KEEPALIVE int execute(int fdinput, int fdoutput) {
|
||||
return 1;
|
||||
}
|
||||
if (strncmp(magic, ARMAG, SARMAG) != 0) {
|
||||
fprintf(stderr, "ERROR file is not of the expected shape");
|
||||
fprintf(stderr, "ERROR bad magic value");
|
||||
fclose(finput);
|
||||
fclose(foutput);
|
||||
return 1;
|
||||
@ -81,6 +81,7 @@ EMSCRIPTEN_KEEPALIVE int execute(int fdinput, int fdoutput) {
|
||||
fseek(finput, (size + 1) & ~1, SEEK_CUR);
|
||||
}
|
||||
|
||||
fprintf(stdout, "hello world!\n");
|
||||
fflush(foutput);
|
||||
fclose(foutput);
|
||||
fclose(finput);
|
||||
11
server/plugin/plg_application_dev/manifest.json
Normal file
11
server/plugin/plg_application_dev/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"author": "Filestash Pty Ltd",
|
||||
"version": "v0.0",
|
||||
"modules": [
|
||||
{
|
||||
"type": "xdg-open",
|
||||
"mime": "application/x-archive",
|
||||
"application": "table"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -91,6 +91,7 @@ func Build(a App) *mux.Router {
|
||||
// Application Resources
|
||||
middlewares = []Middleware{ApiHeaders, SecureHeaders, PluginInjector}
|
||||
r.HandleFunc(WithBase("/api/backend"), NewMiddlewareChain(AdminBackend, middlewares, a)).Methods("GET")
|
||||
r.HandleFunc(WithBase("/api/plugin"), NewMiddlewareChain(PluginExportHandler, middlewares, a)).Methods("GET")
|
||||
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
|
||||
@ -100,6 +101,7 @@ func Build(a App) *mux.Router {
|
||||
} else { // TODO: remove this after migration is done
|
||||
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("/plugin/{name}/{path:.+}"), NewMiddlewareChain(PluginStaticHandler, middlewares, a)).Methods("GET")
|
||||
}
|
||||
|
||||
// Other endpoints
|
||||
|
||||
Reference in New Issue
Block a user