first commit

This commit is contained in:
Giuseppe Naponiello
2026-06-14 19:01:02 +02:00
commit 36bcc9a842
98 changed files with 26936 additions and 0 deletions

18
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,18 @@
# Build context = ./frontend (vedi docker-compose.yml). Il .dockerignore di root
# NON si applica qui: serve questo file dedicato.
# Evita di copiare nell'immagine artefatti pesanti, locali o incompatibili.
# node_modules host = glibc, immagine = Alpine/musl: MAI copiarli nel container.
node_modules
dist
coverage
stats.html
.git
.gitignore
.env
.env.*
!.env.example
*.log
.DS_Store

View File

@@ -0,0 +1,45 @@
# --- STAGE 1: Sviluppo (Node LTS) ---
FROM node:lts-alpine3.24 AS development
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
# In sviluppo il sorgente arriva dal bind-mount (./frontend:/app) e i node_modules
# dall'immagine via volume anonimo (/app/node_modules): NON si copia il sorgente qui.
# Vantaggi: niente build fragile su index.html/src mancanti, nessun COPY ricorsivo
# (docker:S6470) e i node_modules musl dell'immagine schermano quelli glibc dell'host.
COPY package*.json ./
RUN npm install
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# --- STAGE 2: Build (Node LTS) ---
FROM node:lts-alpine3.24 AS build
LABEL maintainer="Giuseppe Naponiello"
LABEL version="3.0"
LABEL description="Dynamic Collections Plus, frontend container (build stage)."
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
COPY package*.json ./
# npm ci richiede package-lock.json (deterministico). Fallback a install se assente.
# Servono le devDependencies (Vite, TypeScript) per compilare: NON usare --omit=dev.
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
# Copie esplicite (no `COPY . .` — docker:S6470): non sovrascrivere i node_modules
# musl appena installati con quelli glibc dell'host, né copiare segreti/file locali.
COPY vite.config.ts tsconfig.json index.html ./
COPY src/ ./src/
COPY public/ ./public/
RUN npm run build
# --- STAGE 3: Produzione (Nginx stable, patchato) ---
FROM nginx:1.30-alpine AS production
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

11
frontend/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0">
<title>Dynamic Collection - Index</title>
</head>
<body>
<script type="module" src="./src/pages/index/index.ts"></script>
</body>
</html>

70
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,70 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# URL puliti — /mappa trova mappa.html
location / {
try_files $uri $uri.html $uri/ =404;
}
# Pagine "scheda" con slug nel path: /<page>/<slug> -> <page>.html
# Lo slug resta leggibile lato client da location.pathname.
# Elencare qui le pagine path-style reali quando esistono (deve esistere il relativo .html).
# Esempio:
# location ~ ^/(?<page>artifact|institution)/[^/]+/?$ {
# try_files /$page.html =404;
# }
# Proxy per API: inoltra /api/* al backend
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Documentazione utente (MkDocs)
location /documentation/ {
proxy_pass http://docs:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Documentazione OpenAPI (Scramble) — /api-docs/* → backend /docs/*
location /api-docs/ {
rewrite ^/api-docs/(.*)$ /docs/$1 break;
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Cache aggressiva per asset con hash (Vite li genera con hash nel nome)
location ~* \.(js|css|woff2|png|jpg|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Blocca file sensibili
location ~ /\. {
deny all;
}
# Storage per file statici
location /storage/ {
alias /usr/share/nginx/html/storage/;
access_log off;
expires 30d;
}
access_log /dev/stdout;
error_log /dev/stderr warn;
}

2481
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
frontend/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "dyncoll-frontend",
"private": true,
"version": "3.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit",
"test": "vitest run --coverage",
"test:watch": "vitest"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.1",
"@types/leaflet": "^1.9.21",
"@types/node": "^25.9.3",
"@vitest/coverage-v8": "^4.1.8",
"daisyui": "^5.5.23",
"rollup-plugin-visualizer": "^7.0.1",
"tailwindcss": "^4.3.1",
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.8"
},
"dependencies": {
"axios": "^1.17.0",
"leaflet": "^1.9.4"
}
}

216
frontend/public/vendor/3dhop/corto.em.js vendored Normal file

File diff suppressed because one or more lines are too long

961
frontend/public/vendor/3dhop/meco.js vendored Normal file
View File

@@ -0,0 +1,961 @@
/*
Nexus
Copyright (c) 2012-2020, Visual Computing Lab, ISTI - CNR
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
onmessage = function(job) {
if(typeof(job.data) == "string") return;
var node = job.data.node;
var signature = job.data.signature;
var patches = job.data.patches;
// var now =new Date().getTime();
var size;
if(!node.buffer) return;
else size = node.buffer.byteLength;
var buffer;
for(var i =0 ; i < 1; i++) {
var coder = new MeshCoder(signature, node, patches);
buffer = coder.decode(node.buffer);
}
node.buffer = buffer;
node.owner = job.owner;
// var elapsed = new Date().getTime() - now;
// var t = node.nface;
// console.log("Z Time: " + elapsed + " Size: " + size + " KT/s " + (t/(elapsed)) + " Mbps " + (8*1000*node.buffer.byteLength/elapsed)/(1<<20));
postMessage(node);
}
// actually bitstreams expects a little endian uin64 type. convert it to 2 uint32
BitStream = function(array) {
this.a = array;
for(var i = 0; i < array.length; i += 2) {
var s = array[i];
array[i] = array[i+1];
array[i+1] = s;
}
this.position = 0;
this.bitsPending = 0;
};
BitStream.prototype = {
read: function(bits) {
var bitBuffer = 0;
while(bits > 0) {
var partial;
var bitsConsumed;
if (this.bitsPending > 0) {
var byte = (this.a[this.position - 1] & (0xffffffff >>> (32 - this.bitsPending)))>>>0;
bitsConsumed = Math.min(this.bitsPending, bits);
this.bitsPending -= bitsConsumed;
partial = byte >>> this.bitsPending;
} else {
bitsConsumed = Math.min(32, bits);
this.bitsPending = 32 - bitsConsumed;
partial = this.a[this.position++] >>> this.bitsPending;
}
bits -= bitsConsumed;
bitBuffer = ((bitBuffer << bitsConsumed) | partial)>>>0;
}
return bitBuffer;
},
replace: function(bits, value) {
//zero last part
value = (value & (0xffffffff >>> 32 - bits)) >>> 0;
value = (value | read(bits)) >>> 0;
return value;
}
};
Stream = function(buffer) {
this.data = buffer;
this.buffer = new Uint8Array(buffer);
this.pos = 0;
}
Stream.prototype = {
readChar: function() {
var c = this.buffer[this.pos++];
if(c > 127) c -= 256;
return c;
},
readUChar: function() {
return this.buffer[this.pos++];
},
readInt: function() {
var c = this.buffer[this.pos + 3]
c <<= 8;
c |= this.buffer[this.pos + 2];
c <<= 8;
c |= this.buffer[this.pos + 1];
c <<= 8;
c |= this.buffer[this.pos + 0];
this.pos += 4;
return c;
},
readArray: function(n) {
var a = this.buffer.subarray(this.pos, this.pos+n);
this.pos += n;
return a;
},
readBitStream:function() {
var n = this.readInt();
var pad = this.pos & 0x3;
if(pad != 0)
this.pos += 4 - pad;
var b = new BitStream(new Uint32Array(this.data, this.pos, n*2));
this.pos += n*8;
return b;
}
};
function Tunstall(wordsize, lookup_size) {
this.wordsize = wordsize? wordsize : 8;
this.lookup_size = lookup_size? lookup_size : 8;
}
Tunstall.prototype = {
decompress: function(stream) {
var nsymbols = stream.readUChar();
this.probabilities = stream.readArray(nsymbols*2);
this.createDecodingTables();
var size = stream.readInt();
var data = new Uint8Array(size);
var compressed_size = stream.readInt();
var compressed_data = stream.readArray(compressed_size);
if(size)
this._decompress(compressed_data, compressed_size, data, size);
return data;
},
createDecodingTables: function() {
//read symbol,prob,symbol,prob as uchar.
//Here probabilities will range from 0 to 0xffff for better precision
var n_symbols = this.probabilities.length/2;
if(n_symbols <= 1) return;
var queues = []; //array of arrays
var buffer = [];
//initialize adding all symbols to queues
for(var i = 0; i < n_symbols; i++) {
var symbol = this.probabilities[i*2];
var s = [(this.probabilities[i*2+1])<<8, buffer.length, 1]; //probability, position in the buffer, length
queues[i] = [s];
buffer.push(this.probabilities[i*2]); //symbol
}
var dictionary_size = 1<<this.wordsize;
var n_words = n_symbols;
var table_length = n_symbols;
//at each step we grow all queues using the most probable sequence
while(n_words < dictionary_size - n_symbols +1) {
//Should use a stack or something to be faster, but we have few symbols
//find highest probability word
var best = 0;
var max_prob = 0;
for(var i = 0; i < n_symbols; i++) {
var p = queues[i][0][0]; //front of queue probability.
if(p > max_prob) {
best = i;
max_prob = p;
}
}
var symbol = queues[best][0];
var pos = buffer.length;
for(var i = 0; i < n_symbols; i++) {
var sym = this.probabilities[i*2];
var prob = this.probabilities[i*2+1]<<8;
var s = [((prob*symbol[0])>>>16), pos, symbol[2]+1]; //combine probabilities, keep track of buffer, keep length of queue
for(var k = 0; k < symbol[2]; k++)
buffer[pos+k] = buffer[symbol[1] + k]; //copy sequence of symbols
pos += symbol[2];
buffer[pos++] = sym; //append symbol
queues[i].push(s);
}
table_length += (n_symbols-1)*(symbol[2] + 1) +1;
n_words += n_symbols -1;
queues[best].shift(); //remove first thing
}
this.index = new Uint32Array(n_words);
this.lengths = new Uint32Array(n_words);
this.table = new Uint8Array(table_length);
var word = 0;
var pos = 0;
for(i = 0; i < queues.length; i++) {
var queue = queues[i];
for(var k = 0; k < queue.length; k++) {
var s = queue[k];
this.index[word] = pos;
this.lengths[word] = s[2]; //length
word++;
for(var j = 0; j < s[2]; j++)
this.table[pos + j] = buffer[s[1] + j]; //buffer of offset
pos += s[2]; //length
}
}
},
_decompress: function(input, input_size, output, output_size) {
var input_pos = 0;
var output_pos = 0;
if(this.probabilities.length == 2) {
var symbol = this.probabilities[0];
for(var i = 0; i < output_size; i++)
output[i] = symbol;
return;
}
while(input_pos < input_size-1) {
var symbol = input[input_pos++];
var start = this.index[symbol];
var end = start + this.lengths[symbol];
for(var i = start; i < end; i++)
output[output_pos++] = this.table[i];
}
//last symbol might override so we check.
var symbol = input[input_pos];
var start = this.index[symbol];
var end = start + output_size - output_pos;
var length = output_size - output_pos;
for(var i = start; i < end; i++)
output[output_pos++] = this.table[i];
return output;
}
}
ZPoint = function(h, l) {
this.lo = l;
this.hi = h;
}
ZPoint.prototype = {
copy: function(z) {
this.lo = z.lo;
this.hi = z.hi;
},
setBit: function(d) {
if(d < 32)
this.lo = (this.lo | (1<<d))>>>0;
else
this.hi = (this.hi | (1<<(d-32)))>>>0;
},
toPoint: function(min, step, buffer, pos) {
var x = this.morton3(this.lo, this.hi>>>1);
var y = this.morton3(this.lo>>>1, this.hi>>>2);
var z = this.morton3((this.lo>>>2 | (this.hi & 0x1)<<30 )>>>0, this.hi>>>3); //first hi bit needs to go into low.
buffer[pos+0] = (x + min[0])*step;
buffer[pos+1] = (y + min[1])*step;
buffer[pos+2] = (z + min[2])*step;
},
morton3: function(lo, hi) {
lo = ( lo & 0x49249249)>>>0;
lo = ((lo | (lo >>> 2 )) & 0xc30c30c3)>>>0;
lo = ((lo | (lo >>> 4 )) & 0x0f00f00f)>>>0;
lo = ((lo | (lo >>> 8 )) & 0xff0000ff)>>>0;
lo = ((lo | (lo >>> 16)) & 0x0000ffff)>>>0;
hi = ( hi & 0x49249249)>>>0;
hi = ((hi | (hi >> 2 )) & 0xc30c30c3)>>>0;
hi = ((hi | (hi >> 4 )) & 0x0f00f00f)>>>0;
hi = ((hi | (hi >> 8 )) & 0xff0000ff)>>>0;
hi = ((hi | (hi >> 16)) & 0x0000ffff)>>>0;
return ((hi<<11) | lo)>>>0;
}
};
//node is an object with nvert, nface
//patches is an array of offsets in the index, triangle are grouped by those offsets
//signature tells wether mesh has indices, normals, colors, etc. {'colors': true, 'normals':true, 'indices': true }
function MeshCoder(signature, node, patches) {
this.sig = signature;
this.node = node;
this.patches = patches;
this.last = new Int32Array(this.node.nvert);
this.last_count = 0;
}
MeshCoder.prototype = {
//assumes input is an ArrayBuffer
decode: function(input) {
var t = this;
t.buffer = new ArrayBuffer(t.node.nvert*(12 + t.sig.texcoords*8 + t.sig.normals*6 + t.sig.colors*4) + t.node.nface*t.sig.indices*6);
var size = t.node.nvert*12; //float
t.coords = new Float32Array(t.buffer, 0, t.node.nvert*3);
if(t.sig.texcoords) {
t.texcoords = new Float32Array(t.buffer, size, t.node.nvert*2);
size += t.node.nvert*8; //float
}
if(t.sig.normals) {
t.normals = new Int16Array(t.buffer, size, t.node.nvert*3);
size += t.node.nvert*6; //short
}
if(t.sig.colors) {
t.colors = new Uint8ClampedArray(t.buffer, size, t.node.nvert*4);
size += t.node.nvert*4; //chars
}
if(t.sig.indices) {
t.faces = new Uint16Array(t.buffer, size, t.node.nface*3);
size += t.node.nface*6; //short
}
t.stream = new Stream(input);
t.stack = new Float32Array(12); //min0, min1, min2, step, tmin0, tmin1, tstep
t.stack[3] = t.stream.readInt();
t.stack[4] = t.stream.readInt();
t.stack[5] = t.stream.readInt();
t.coord_q = t.stream.readChar();
t.coord_bits = t.stream.readChar()*3;
t.stack[6] = Math.pow(2.0, t.coord_q);
if(t.sig.texcoords) {
t.stack[9] = t.stream.readInt();
t.stack[10] = t.stream.readInt();
t.texcoord_q = t.stream.readChar();
t.texcoord_bits = t.stream.readChar()*2;
t.stack[11] = Math.pow(2.0, t.texcoord_q);
}
if(t.sig.indices) {
t.decodeFaces();
// var faces = window.performance.now() - start;
// start += faces;
} else {
t.decodeCoordinates();
// var coords = window.performance.now() - start;
// start += coords;
}
if(t.sig.normals)
t.decodeNormals();
// var normals = window.performance.now() - start;
// start += normals;
if(t.sig.colors)
t.decodeColors();
// var colors = window.performance.now() - start;
// start += colors;
// console.log("Decode " + (faces + coords + normals + colors) + "ms. C: " + coords + " F: " + faces + " N: " + normals + " C: " + colors);
return t.buffer;
},
decodeCoordinates: function() {
var t = this;
t.min = [t.stack[3], t.stack[4], t.stack[5]];
var step = Math.pow(2.0, t.coord_q);
var hi_bits = Math.max(t.coord_bits - 32, 0);
var lo_bits = Math.min(t.coord_bits, 32);
var bitstream = t.stream.readBitStream();
var tunstall = new Tunstall;
var diffs = tunstall.decompress(t.stream);
var hi = bitstream.read(hi_bits);
var lo = bitstream.read(lo_bits);
var p = new ZPoint(hi, lo);
var count = 0;
p.toPoint(t.min, step, t.coords, count);
count += 3;
for(var i = 1; i < t.node.nvert; i++) {
var d = diffs[i-1];
p.setBit(d, 1);
if(d > 32) {
p.hi = (p.hi & ~((1<<(d-32))-1))>>>0;
var e = bitstream.read(d - 32);
p.hi = (p.hi | e)>>>0;
p.lo = bitstream.read(32);
} else {
if(d == 32) {
p.lo = bitstream.read(d);
} else {
var e = bitstream.read(d);
p.lo = (p.lo & ~((1<<d) -1))>>>0;
p.lo = (p.lo | e)>>>0;
}
}
p.toPoint(t.min, step, t.coords, count);
count += 3;
}
},
decodeFaces: function() {
if(!this.node.nface) return;
this.vertex_count = 0;
var start = 0;
for(var p = 0; p < this.patches.length; p++) {
var end = this.patches[p];
this.decodeConnectivity(end - start, start*3);
start = end;
}
//dequantize positions
var tot = this.node.nvert*3;
var coords = this.coords;
var stack = this.stack;
for(var i = 0; i < tot; ) {
coords[i] = (coords[i] + stack[3])*stack[6]; i++;
coords[i] = (coords[i] + stack[4])*stack[6]; i++;
coords[i] = (coords[i] + stack[5])*stack[6]; i++;
}
if(this.sig.texcoords) {
var t_tot = this.node.nvert*2;
var t_coords = this.texcoords;
for(var i = 0; i < tot; ) {
t_coords[i] = (t_coords[i] + stack[9])*stack[11]; i++;
t_coords[i] = (t_coords[i] + stack[10])*stack[11]; i++;
}
}
},
decodeNormals: function() {
var norm_q = this.stream.readChar();
var dtunstall = new Tunstall;
var diffs = dtunstall.decompress(this.stream);
var stunstall = new Tunstall;
var signs = stunstall.decompress(this.stream);
var bitstream = this.stream.readBitStream();
var side = (1<<(16 - norm_q))>>>0;
var diffcount = 0;
var signcount = 0;
if(!this.sig.indices) {
for(var k = 0; k < 2; k++) {
var on = 0;
for(var i = 0; i < this.node.nvert; i++) {
var d = this.decodeDiff(diffs[diffcount++], bitstream);
on = on + d;
this.normals[3*i + k] = on*side;
}
}
for(var i = 0; i < this.node.nvert; i++) {
var offset = i*3;
var x = this.normals[offset + 0];
var y = this.normals[offset + 1];
var z = 32767.0*32767.0 - x*x - y*y;
if(z < 0) z = 0;
z = Math.sqrt(z);
if(z > 32767) z = 32767;
if(signs[i] == 0)
z = -z;
this.normals[offset + 2] = z;
}
return;
}
var boundary = this.markBoundary();
this.computeNormals();
if(this.sig.texcoords) //hack, fixing normals makes it worse actually
return;
var stat = 0;
//get difference between original and predicted
for(var i = 0; i < this.node.nvert; i++) {
if(!boundary[i]) continue;
var offset = i*3;
var x = (this.normals[offset + 0]/side);
var y = (this.normals[offset + 1]/side);
var dx = this.decodeDiff(diffs[diffcount++], bitstream);
var dy = this.decodeDiff(diffs[diffcount++], bitstream);
x = (x + dx)*side;
y = (y + dy)*side;
var z = 32767.0*32767.0 - x*x - y*y;
if(z < 0) z = 0;
z = Math.sqrt(z);
//sign
if(z > 32767.0) z = 32767.0;
var signbit = signs[signcount++];
// if(this.normals[offset+2] < 0 != signbit)
if((this.normals[offset+2] < 0 && signbit == 0) || (this.normals[offset+2] > 0 && signbit == 1))
z = -z;
this.normals[offset + 0] = x;
this.normals[offset + 1] = y;
this.normals[offset + 2] = z;
}
},
decodeColors: function() {
var color_q = [];
for(var k = 0; k < 4; k++)
color_q[k] = this.stream.readChar();
var diffs = [];
for(var k = 0; k < 4; k++) {
var tunstall = new Tunstall;;
diffs[k] = tunstall.decompress(this.stream);
}
var bitstream = this.stream.readBitStream();
var count = 0;
if(this.sig.indices) {
for(var i = 0; i < this.node.nvert; i++) {
var last = this.last[i]*4;
var offset = i*4;
for(var k = 0; k < 4; k++) {
var c = this.decodeDiff(diffs[k][count], bitstream);
if(last >= 0)
c += this.colors[last + k];
this.colors[offset] = c;
offset++;
}
count++;
}
} else {
for(var k = 0; k < 4; k++)
this.colors[k] = this.decodeDiff(diffs[k][count], bitstream);
count++;
var offset = 4;
for(var i = 1; i < this.node.nvert; i++) {
for(var k = 0; k < 4; k++) {
var d = this.decodeDiff(diffs[k][count], bitstream);
this.colors[offset] = this.colors[offset-4] + d;
offset ++;
}
count++;
}
}
var steps = [];
for(var k = 0; k < 4; k++)
steps[k] = (1<<(8 - color_q[k]));
//convert to rgb
for(var i = 0; i < this.node.nvert; i++) {
var offset = i*4;
var e0 = this.colors[offset + 0] * steps[0];
var e1 = this.colors[offset + 1] * steps[1];
var e2 = this.colors[offset + 2] * steps[2];
var e3 = this.colors[offset + 3] * steps[3];
this.colors[offset + 0] = (e2 + e0)&0xff;
this.colors[offset + 1] = e0;
this.colors[offset + 2] = (e1 + e0)&0xff;
this.colors[offset + 3] = e3;
}
},
//how to determine if a vertex is a boundary without topology:
//for each edge a vertex is in, add or subtract the id of the other vertex depending on order
//for internal vertices sum is zero.
//unless we have strange configurations and a lot of sfiga, zero wont happen. //TODO think about this
markBoundary: function() {
// var boundary = new Uint8Array(this.node.nvert);
var count = new Uint32Array(this.node.nvert);
var offset = 0;
for(var i = 0; i < this.node.nface; i++) {
count[this.faces[offset + 0]] += this.faces[offset + 1] - this.faces[offset + 2];
count[this.faces[offset + 1]] += this.faces[offset + 2] - this.faces[offset + 0];
count[this.faces[offset + 2]] += this.faces[offset + 0] - this.faces[offset + 1];
offset += 3;
}
return count;
// for(var i = 0; i < this.node.nvert; i++)
// if(count[i] != 0)
// boundary[i] = true;
// return boundary;
},
norm: function(buffer, a, b, c) { //a b c offsets in the buffer
var ba0 = buffer[b+0] - buffer[a+0];
var ba1 = buffer[b+1] - buffer[a+1];
var ba2 = buffer[b+2] - buffer[a+2];
var ca0 = buffer[c+0] - buffer[a+0];
var ca1 = buffer[c+1] - buffer[a+1];
var ca2 = buffer[c+2] - buffer[a+2];
var p = [];
p[0] = ba1*ca2 - ba2*ca1;
p[1] = ba2*ca0 - ba0*ca2;
p[2] = ba0*ca1 - ba1*ca0;
return p;
},
normalize: function(buffer, offset) {
var x = buffer[offset + 0];
var y = buffer[offset + 1];
var z = buffer[offset + 2];
var n = Math.sqrt(x*x + y*y + z*z);
if(n > 0) {
buffer[offset + 0] = x/n;
buffer[offset + 1] = y/n;
buffer[offset + 2] = z/n;
}
},
computeNormals:function() {
var tmp_normals = new Float32Array(this.node.nvert*3);
var offset = 0;
for(var i = 0; i < this.node.nface; i++) {
var a = 3*this.faces[offset + 0];
var b = 3*this.faces[offset + 1];
var c = 3*this.faces[offset + 2];
var buffer = this.coords;
var ba0 = buffer[b+0] - buffer[a+0];
var ba1 = buffer[b+1] - buffer[a+1];
var ba2 = buffer[b+2] - buffer[a+2];
var ca0 = buffer[c+0] - buffer[a+0];
var ca1 = buffer[c+1] - buffer[a+1];
var ca2 = buffer[c+2] - buffer[a+2];
var n0 = ba1*ca2 - ba2*ca1;
var n1 = ba2*ca0 - ba0*ca2;
var n2 = ba0*ca1 - ba1*ca0;
tmp_normals[a + 0] += n0;
tmp_normals[a + 1] += n1;
tmp_normals[a + 2] += n2;
tmp_normals[b + 0] += n0;
tmp_normals[b + 1] += n1;
tmp_normals[b + 2] += n2;
tmp_normals[c + 0] += n0;
tmp_normals[c + 1] += n1;
tmp_normals[c + 2] += n2;
offset += 3;
}
//normalize
var offset = 0;
for(var i = 0; i < this.node.nvert; i++) {
var x = tmp_normals[offset + 0];
var y = tmp_normals[offset + 1];
var z = tmp_normals[offset + 2];
var n = Math.sqrt(x*x + y*y + z*z);
if(n > 0) {
tmp_normals[offset + 0] = x/n;
tmp_normals[offset + 1] = y/n;
tmp_normals[offset + 2] = z/n;
}
this.normals[offset + 0] = tmp_normals[offset + 0]*32767;
this.normals[offset + 1] = tmp_normals[offset + 1]*32767;
this.normals[offset + 2] = tmp_normals[offset + 2]*32767;
offset += 3;
}
},
decodeDiff: function(diff, bitstream) {
var val;
if(diff == 0) {
val = 1;
} else {
val = 1<<(diff);
val |= bitstream.read(diff);
};
val--; //vall is always >= 1
if(val & 0x1)
val = -((val+1)>>1);
else
val = val>>1;
return val;
},
/* an edge is: uint16_t face, uint16_t side, uint32_t prev, next, bool deleted
I do not want to create millions of small objects, I will use aUint32Array.
Problem is how long, sqrt(nface) we will over blow using nface.
*/
decodeConnectivity: function(length, start) {
var t = this;
var ctunstall = new Tunstall;
var clers = ctunstall.decompress(this.stream);
var cler_count = 0;
var dtunstall = new Tunstall;
var diffs = dtunstall.decompress(this.stream);
var diff_count = 0;
var tdiffs;
var tdiff_count = 0;
if(t.sig.texcoords) {
var ttunstall = new Tunstall;
tdiffs = ttunstall.decompress(this.stream);
}
var bitstream = this.stream.readBitStream(bitstream);
var current_face = 0; //keep track of connected component start
//t.vertex_count = 0;
var front = new Uint32Array(this.node.nface*18);
var front_count = 0; //count each integer so it's front_back*5
function addFront(_v0, _v1, _v2, _prev, _next) {
front[front_count++] = _v0;
front[front_count++] = _v1;
front[front_count++] = _v2;
front[front_count++] = _prev;
front[front_count++] = _next;
front[front_count++] = 0; //deleted
}
function _next(t) {
t++;
if(t == 3) t = 0;
return t;
}
function _prev(t) {
t--;
if(t == -1) t = 2;
return t;
}
var delayed = [];
var faceorder = [];
var faces_count = start; //count indices in this.faces array
var totfaces = length;
// var estimated = [0, 0, 0]; //no! use stack.
var stack = this.stack;
var coords = this.coords;
var texcoords = this.texcoords;
var hasTexCoords = t.sig.texcoords;
while(totfaces > 0) {
if(!faceorder.length && !delayed.length) {
if(current_face == this.node.nface) break; //no more faces to encode exiting
stack[0] = stack[1] = stack[2] = 0;
stack[7] = stack[8] = 0; //texcoords
var last_index = -1;
var index = [];
for(var k = 0; k < 3; k++) {
this.last[this.last_count++] = last_index;
var diff = diffs[diff_count++];
var tdiff = diff && hasTexCoords? tdiffs[tdiff_count++] : 0;
var v = this.decodeVertex(bitstream, diff, tdiff);
index[k] = v;
this.faces[faces_count++] = v;
stack[0] = coords[v*3];
stack[1] = coords[v*3+1];
stack[2] = coords[v*3+2];
if(t.sig.texcoords) {
stack[7] = texcoords[v*2];
stack[8] = texcoords[v*2+1];
}
last_index = v;
}
var current_edge = front_count;
for(var k = 0; k < 3; k++) {
faceorder.push(front_count);
front[front_count++] = index[_next(k)];
front[front_count++] = index[_prev(k)];
front[front_count++] = index[k];
front[front_count++] = current_edge + _prev(k)*6;
front[front_count++] = current_edge + _next(k)*6;
front_count++;
// addFront(index[_next(k)], index[_prev(k)], index[k], current_edge + _prev(k)*6, current_edge + _next(k)*6);
}
current_face++;
totfaces--;
continue;
}
var f;
if(faceorder.length)
f = faceorder.shift();
else
f = delayed.pop();
var edge_start = f;
if(front[edge_start + 5]) continue; //deleted
front[edge_start + 5] = 1; //set edge as deleted anyway
var c = clers[cler_count++];
if(c == 4) continue; //BOUNDARY
var v0 = front[edge_start + 0];
var v1 = front[edge_start + 1];
var v2 = front[edge_start + 2];
var prev = front[edge_start + 3];
var next = front[edge_start + 4];
var first_edge = front_count; //points to new edge to be inserted
var opposite = -1;
if(c == 0) { //VERTEX
//predict position based on v0, v1 and v2
for(var k = 0; k < 3; k++)
stack[k] = coords[v0*3 + k] + coords[v1*3 + k] - coords[v2*3 + k];
if(hasTexCoords)
for(var k = 0; k < 2; k++)
stack[7+k] = texcoords[v0*2 + k] + texcoords[v1*2 + k] - texcoords[v2*2 + k];
var diff = diffs[diff_count++];
var tdiff = diff && hasTexCoords? tdiffs[tdiff_count++] : 0;
opposite = this.decodeVertex(bitstream, diff, tdiff);
if(diff != 0)
this.last[this.last_count++] = v1;
front[prev + 4] = first_edge;
front[next + 3] = first_edge + 6;
faceorder.unshift(front_count);
front[front_count++] = v0;
front[front_count++] = opposite;
front[front_count++] = v1;
front[front_count++] = prev;
front[front_count++] = first_edge+6;
front_count++;
// addFront(v0, opposite, v1, prev, first_edge + 6);
faceorder.push(front_count);
front[front_count++] = opposite;
front[front_count++] = v1;
front[front_count++] = v0;
front[front_count++] = first_edge;
front[front_count++] = next;
front_count++;
// addFront(opposite, v1, v0, first_edge, next);
} else if(c == 3) { //END
front[prev + 5] = 1;
front[next + 5] = 1;
front[front[prev + 3] + 4] = front[next + 4];
front[front[next + 4] + 3] = front[prev + 3];
opposite = front[prev + 0];
} else if(c == 1) { //LEFT
front[prev + 5] = 1; //deleted
front[front[prev + 3] + 4] = first_edge;
front[next + 3] = first_edge;
opposite = front[prev + 0];
faceorder.unshift(front_count);
front[front_count++] = opposite;
front[front_count++] = v1;
front[front_count++] = v0;
front[front_count++] = front[prev +3];
front[front_count++] = next;
front_count++;
// addFront(opposite, v1, v0, front[prev + 3], next);
} else if(c == 2) { //RIGHT
front[next + 5] = 1;
front[front[next + 4] + 3] = first_edge;
front[prev + 4] = first_edge;
opposite = front[next + 1];
faceorder.unshift(front_count);
front[front_count++] = v0;
front[front_count++] = opposite;
front[front_count++] = v1;
front[front_count++] = prev;
front[front_count++] = front[next+4];
front_count++;
// addFront(v0, opposite, v1, prev, front[next + 4]);
} else if(c == 5) { //DELAY
front[edge_start + 5] = 0;
delayed.push(edge_start);
continue;
}
this.faces[faces_count++] = v1;
this.faces[faces_count++] = v0;
this.faces[faces_count++] = opposite;
totfaces--;
}
},
decodeVertex: function(bitstream, diff, tdiff) {
if(diff == 0)
return bitstream.read(16);
var v = this.vertex_count++;
var max = 1<<(diff-1);
for(var k = 0; k < 3; k++) {
var d = bitstream.read(diff) - max;
this.coords[v*3+k] = this.stack[k] + d; //stack 0-3 is used as extimated
}
if(this.sig.texcoords) {
var tmax = 1<<(tdiff-1);
for(var k = 0; k < 2; k++) {
var d = bitstream.read(tdiff) - tmax;
this.texcoords[v*2+k] = this.stack[7+k] + d; //stack 7-9 is used as extimated
}
}
return v;
},
decodeDiff: function(diff, bitstream) {
var val;
if(diff == 0) {
return 0;
}
val = 1<<diff;
val += bitstream.read(diff);
if(val & 0x1)
val >>>= 1;
else
val = -(val>>>1);
return val;
}
};
var tot = 0;

1323
frontend/public/vendor/3dhop/nexus.js vendored Normal file

File diff suppressed because it is too large Load Diff

1042
frontend/public/vendor/3dhop/ply.js vendored Normal file

File diff suppressed because it is too large Load Diff

4232
frontend/public/vendor/3dhop/presenter.js vendored Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,383 @@
/*
3DHOP - 3D Heritage Online Presenter
Copyright (c) 2014-2023, Visual Computing Lab, ISTI - CNR
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Constructs a PanTiltTrackball object.
* @class Interactor which implements a pan-tilt trackball controller with bounds.
*/
function PanTiltTrackball() {
}
PanTiltTrackball.prototype = {
setup : function (options,myPresenter) {
options = options || {};
var opt = sglGetDefaultObject({
startCenter : [ 0.0, 0.0, 0.0 ],
startPanX : 0.0,
startPanY : 0.0,
startAngleX : 0.0,
startAngleY : 0.0,
startDistance : 2.0,
minMaxDist : [0.2, 4.0],
minMaxPanX : [-0.7, 0.7],
minMaxPanY : [-0.7, 0.7],
minMaxAngleX : [-70.0, 70.0],
minMaxAngleY : [-70.0, 70.0],
pathStates : [ ], // path points array
animationLocked : false,// if true disable trackball interactions during animation
animationTime : null // when single position navigation, each to point navigation is # seconds (if null, automatically computed)
}, options);
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._matrix = SglMat4.identity();
this.myPresenter = myPresenter;// parent presenter
// path
this._pathStates = opt.pathStates;
this._animationLocked = opt.animationLocked;
this._pathPosNum = 0;
// trackball center
this._center = opt.startCenter;
// starting/default parameters
this._startPanX = opt.startPanX; //pan X
this._startPanY = opt.startPanY; //pan Y
this._startAngleX = sglDegToRad(opt.startAngleX); //angle X
this._startAngleY = sglDegToRad(opt.startAngleY); //angle Y
this._startDistance = opt.startDistance; //distance
// current parameters
this._panX = this._startPanX;
this._panY = this._startPanY;
this._angleX = this._startAngleX;
this._angleY = this._startAngleY;
this._distance = this._startDistance;
// target paramenters
this._targetPanX = this._startPanX;
this._targetPanY = this._startPanY;
this._targetAngleX = this._startAngleX;
this._targetAngleY = this._startAngleY;
this._targetDistance = this._startDistance;
//animation data
this._isAnimating = false;
this._speedPanX = 0.0;
this._speedPanY = 0.0;
this._speedAngleX = 0.0;
this._speedAngleY = 0.0;
this._speedDistance = 0.0;
this._isAutoWalking = false;
this._animationTime = opt.animationTime;
//limits
this._minMaxDist = opt.minMaxDist;
this._minMaxPanX = opt.minMaxPanX;
this._minMaxPanY = opt.minMaxPanY;
this._minMaxAngleX = opt.minMaxAngleX;
this._minMaxAngleX[0] = sglDegToRad(this._minMaxAngleX[0]);
this._minMaxAngleX[1] = sglDegToRad(this._minMaxAngleX[1]);
this._minMaxAngleY = opt.minMaxAngleY;
this._minMaxAngleY[0] = sglDegToRad(this._minMaxAngleY[0]);
this._minMaxAngleY[1] = sglDegToRad(this._minMaxAngleY[1]);
this._start = [0.0, 0.0];
this.reset();
},
_clamp : function(value, low, high) {
if(value < low) return low;
if(value > high) return high;
return value;
},
_computeMatrix: function() {
var m = SglMat4.identity();
// centering
m = SglMat4.mul(m, SglMat4.translation([-this._center[0], -this._center[1], -this._center[2]]));
// zoom
m = SglMat4.mul(m, SglMat4.translation([0.0, 0.0, -this._distance]));
// tilt
m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._angleY, [1.0, 0.0, 0.0]));
m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._angleX, [0.0, -1.0, 0.0]));
// pan
m = SglMat4.mul(m, SglMat4.translation([this._panX, this._panY, 0.0]));
this._matrix = m;
if(typeof onTrackballUpdate != "undefined")
onTrackballUpdate(this.getState());
},
getState : function () {
return [this._panX, this._panY, sglRadToDeg(this._angleX), sglRadToDeg(this._angleY), this._distance];
},
setState : function (newstate) {
// stop animation
this._isAnimating = this._isAutoWalking = false;
this._panX = newstate[0];
this._panY = newstate[1];
this._angleX = sglDegToRad(newstate[2]);
this._angleY = sglDegToRad(newstate[3]);
this._distance = newstate[4];
//check limits
this._panX = this._clamp(this._panX, this._minMaxPanX[0], this._minMaxPanX[1]);
this._panY = this._clamp(this._panY, this._minMaxPanY[0], this._minMaxPanY[1]);
this._angleX = this._clamp(this._angleX, this._minMaxAngleX[0], this._minMaxAngleX[1]);
this._angleY = this._clamp(this._angleY, this._minMaxAngleY[0], this._minMaxAngleY[1]);
this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
this._computeMatrix();
},
animateToState : function (newstate, newtime) {
// stop animation
this._isAnimating = false;
if(newstate)
{
// stop autoWalking
this._isAutoWalking = false;
// setting targets
this._targetPanX = newstate[0];
this._targetPanY = newstate[1];
this._targetAngleX = sglDegToRad(newstate[2]);
this._targetAngleY = sglDegToRad(newstate[3]);
this._targetDistance = newstate[4];
//check limits
this._targetPanX = this._clamp(this._targetPanX, this._minMaxPanX[0], this._minMaxPanX[1]);
this._targetPanY = this._clamp(this._targetPanY, this._minMaxPanY[0], this._minMaxPanY[1]);
this._targetAngleX = this._clamp(this._targetAngleX, this._minMaxAngleX[0], this._minMaxAngleX[1]);
this._targetAngleY = this._clamp(this._targetAngleY, this._minMaxAngleY[0], this._minMaxAngleY[1]);
this._targetDistance = this._clamp(this._targetDistance, this._minMaxDist[0], this._minMaxDist[1]);
// setting base velocities
this._speedPanX = 2.0;
this._speedPanY = 2.0;
this._speedAngleX = Math.PI;
this._speedAngleY = Math.PI;
this._speedDistance = 2.0;
// find max animation time to set a time limit and then synchronize all movements
var timePanX = Math.abs((this._targetPanX - this._panX) / this._speedPanX);
var timePanY = Math.abs((this._targetPanY - this._panY) / this._speedPanY);
var timeAngleX = Math.abs((this._targetAngleX - this._angleX) / this._speedAngleX);
var timeAngleY = Math.abs((this._targetAngleY - this._angleY) / this._speedAngleY);
var timeDistance = Math.abs((this._targetDistance - this._distance) / this._speedDistance);
var maxtime = Math.max( timePanX, Math.max( timePanY, Math.max( timeAngleX, Math.max( timeAngleY, timeDistance ) ) ));
var animationtime = this._clamp(maxtime, 0.5, 2.0);
if(newtime) animationtime = newtime;
else if (this._animationTime) animationtime = this._animationTime;
this._speedPanX *= timePanX / animationtime;
this._speedPanY *= timePanY / animationtime;
this._speedAngleX *= timeAngleX / animationtime;
this._speedAngleY *= timeAngleY / animationtime;
this._speedDistance *= timeDistance / animationtime;
}
else
{
if(this._pathPosNum == this._pathStates.length){
this._isAutoWalking = false;
this._pathPosNum = 0;
}
else {
var state = this._pathStates[this._pathPosNum][0];
var time = this._animationTime;
if(!Array.isArray(state)) state = this._pathStates[this._pathPosNum];
else if (this._pathStates[this._pathPosNum][1]) time = this._pathStates[this._pathPosNum][1];
if(!this._isAutoWalking) this.animateToState(state, time);
this._isAutoWalking = true;
}
}
// start animation
this._isAnimating = true;
},
recenter : function (newpoint) {
// stop animation
this._isAnimating = this._isAutoWalking = false;
var newpanX = -(newpoint[0]-this.myPresenter.sceneCenter[0]) * this.myPresenter.sceneRadiusInv;
var newpanY = -(newpoint[1]-this.myPresenter.sceneCenter[1]) * this.myPresenter.sceneRadiusInv;
this.animateToState([newpanX, newpanY, sglRadToDeg(this._angleX), sglRadToDeg(this._angleY), (this._distance * 0.6)]);
},
tick : function (dt) {
if(!this._isAnimating) return false;
var deltaPanX = this._speedPanX * dt;
var deltaPanY = this._speedPanY * dt;
var deltaAngleX = this._speedAngleX * dt;
var deltaAngleY = this._speedAngleY * dt;
var deltaDistance = this._speedDistance * dt;
var diffPanX = this._targetPanX - this._panX;
var diffPanY = this._targetPanY - this._panY;
var diffAngleX = this._targetAngleX - this._angleX;
var diffAngleY = this._targetAngleY - this._angleY;
var diffDistance = this._targetDistance - this._distance;
if (diffPanX > deltaPanX)
this._panX += deltaPanX;
else if (diffPanX < -deltaPanX)
this._panX -= deltaPanX;
else
this._panX = this._targetPanX;
if (diffPanY > deltaPanY)
this._panY += deltaPanY;
else if (diffPanY < -deltaPanY)
this._panY -= deltaPanY;
else
this._panY = this._targetPanY;
if (diffAngleX > deltaAngleX)
this._angleX += deltaAngleX;
else if (diffAngleX < -deltaAngleX)
this._angleX -= deltaAngleX;
else
this._angleX = this._targetAngleX;
if (diffAngleY > deltaAngleY)
this._angleY += deltaAngleY;
else if (diffAngleY < -deltaAngleY)
this._angleY -= deltaAngleY;
else
this._angleY = this._targetAngleY;
if (diffDistance > deltaDistance)
this._distance += deltaDistance;
else if (diffDistance < -deltaDistance)
this._distance -= deltaDistance;
else
this._distance = this._targetDistance;
if(this._panX == this._targetPanX)
if(this._panY == this._targetPanY)
if(this._angleX == this._targetAngleX)
if(this._angleY == this._targetAngleY)
if(this._distance == this._targetDistance){
this._isAnimating = false;
if(typeof onTrackballArrived != "undefined")
onTrackballArrived(this.getState());
if(this._isAutoWalking) { this._pathPosNum++; this._isAutoWalking = false; this.animateToState(); }
}
this._computeMatrix();
return true;
},
set action(a) { if(this._action != a) { this._new_action = true; this._action = a; } },
get action() { return this._action; },
get matrix() { this._computeMatrix(); return this._matrix; },
get distance() { return this._distance; },
reset : function () {
this._matrix = SglMat4.identity();
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._panX = this._startPanX;
this._panY = this._startPanY;
this._angleX = this._startAngleX;
this._angleY = this._startAngleY;
this._distance = this._startDistance;
this._pathPosNum = 0;
this._isAutoWalking = false;
this._isAnimating = false;
this._computeMatrix();
},
track : function(m, x, y, z) {
if(this._animationLocked && this._isAnimating) this._action = SGL_TRACKBALL_NO_ACTION;
if(this._new_action) {
this._start[0] = x;
this._start[1] = y;
this._new_action = false;
}
var dx = this._start[0] - x;
var dy = this._start[1] - y;
this._start[0] = x;
this._start[1] = y;
switch (this._action) {
case SGL_TRACKBALL_ROTATE:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.rotate(m, dx, dy);
break;
case SGL_TRACKBALL_PAN:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.pan(m, dx, dy);
break;
case SGL_TRACKBALL_SCALE:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.scale(m, z);
break;
default:
break;
}
return this._computeMatrix();
},
rotate: function(m, dx, dy) {
this._angleX += dx;
this._angleY += dy;
this._angleX = this._clamp(this._angleX, this._minMaxAngleX[0], this._minMaxAngleX[1]);
this._angleY = this._clamp(this._angleY, this._minMaxAngleY[0], this._minMaxAngleY[1]);
},
pan: function(m, dx, dy) {
var panSpeed = Math.max(Math.min(1.5, this._distance),0.05);
this._panX -= dx/2.0 * panSpeed;
this._panY -= dy/2.0 * panSpeed;
this._panX = this._clamp(this._panX, this._minMaxPanX[0], this._minMaxPanX[1]);
this._panY = this._clamp(this._panY, this._minMaxPanY[0], this._minMaxPanY[1]);
},
scale : function(m, s) {
this._distance *= s;
this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
}
};
/***********************************************************************/

View File

@@ -0,0 +1,241 @@
/*
3DHOP - 3D Heritage Online Presenter
Copyright (c) 2014-2020, Visual Computing Lab, ISTI - CNR
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Constructs a SphereTrackball object.
* @class Interactor which implements a full spherical trackball controller.
*/
function SphereTrackball() {
}
SphereTrackball.prototype = {
setup : function (options) {
options = options || {};
var opt = sglGetDefaultObject({
startCenter : [ 0.0, 0.0, 0.0 ],
startDistance : 2.0,
minMaxDist : [0.2, 4.0],
}, options);
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._matrix = SglMat4.identity();
this._sphereMatrix = SglMat4.identity();
// starting/default parameters
this._startDistance = opt.startDistance; //distance
// current parameters
this._distance = this._startDistance;
//limits
this._minMaxDist = opt.minMaxDist;
this._pts = [ [0.0, 0.0], [0.0, 0.0] ];
this._start = [0.0, 0.0];
this.reset();
},
clamp : function(value, low, high) {
if(value < low) return low;
if(value > high) return high;
return value;
},
_computeMatrix: function() {
var m = SglMat4.identity();
// zoom
m = SglMat4.mul(m, SglMat4.translation([0.0, 0.0, -this._distance]));
// spheretrack
m = SglMat4.mul(m, this._sphereMatrix);
this._matrix = m;
if(typeof onTrackballUpdate != "undefined")
onTrackballUpdate(this.getState());
},
_projectOnSphere : function(x, y) {
var r = 1.0;
var z = 0.0;
var d = sglSqrt(x*x + y*y);
if (d < (r * 0.70710678118654752440)) {
/* Inside sphere */
z = sglSqrt(r*r - d*d);
}
else {
/* On hyperbola */
t = r / 1.41421356237309504880;
z = t*t / d;
}
return z;
},
_transform : function(m, x, y, z) {
return SglMat4.mul4(m, [x, y, z, 0.0]);
},
_transformOnSphere : function(m, x, y) {
var z = this._projectOnSphere(x, y); //get z value
return this._transform(m, x, y, z);
},
_translate : function(offset, f) {
var invMat = SglMat4.inverse(this._sphereMatrix);
var t = SglVec3.to4(offset, 0.0);
t = SglMat4.mul4(invMat, t);
t = SglVec4.muls(t, f);
var trMat = SglMat4.translation(t);
this._sphereMatrix = SglMat4.mul(this._sphereMatrix, trMat);
},
getState : function () {
return this._sphereMatrix;
},
setState : function (newstate) {
this._sphereMatrix = newstate;
this._computeMatrix();
},
animateToState : function (newstate) {
this._sphereMatrix = newstate;
this._computeMatrix();
},
recenter : function (newpoint) {
var newpanX = (newpoint[0]-presenter.sceneCenter[0]) * presenter.sceneRadiusInv;
var newpanY = (newpoint[1]-presenter.sceneCenter[1]) * presenter.sceneRadiusInv;
var newpanZ = (newpoint[2]-presenter.sceneCenter[2]) * presenter.sceneRadiusInv;
this._sphereMatrix[12] = -newpanX;
this._sphereMatrix[13] = -newpanY;
this._sphereMatrix[14] = -newpanZ;
this._distance *= 0.6;
this._distance = this.clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
this._computeMatrix();
},
tick : function (dt) {
return false;
},
set action(a) { if(this._action != a) this._new_action = true; this._action = a; },
get action() { return this._action; },
get matrix() { this._computeMatrix(); return this._matrix; },
get distance() { return this._distance; },
reset : function () {
this._matrix = SglMat4.identity();
this._sphereMatrix = SglMat4.identity();
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._distance = this._startDistance;
this._pts = [ [0.0, 0.0], [0.0, 0.0] ];
this._computeMatrix();
},
track : function(m, x, y, z) {
if(this._new_action) {
this._start[0] = x;
this._start[1] = y;
this._new_action = false;
}
var dx = this._start[0] - x;
var dy = this._start[1] - y;
this._start[0] = x;
this._start[1] = y;
this._pts[0][0] = this._pts[1][0] + dx;
this._pts[0][1] = this._pts[1][1] + dy;
this._pts[1][0] = dx;
this._pts[1][1] = dy;
switch (this._action) {
case SGL_TRACKBALL_ROTATE:
this.rotate(m);
break;
case SGL_TRACKBALL_PAN:
this.pan(m);
break;
case SGL_TRACKBALL_DOLLY:
this.dolly(m, z);
break;
case SGL_TRACKBALL_SCALE:
this.scale(m, z);
break;
default:
break;
}
},
rotate : function(m) {
if ((this._pts[0][0] == this._pts[1][0]) && (this._pts[0][1] == this._pts[1][1])) return; //if Xold == Xnew && Yold ==Ynew return
var mInv = SglMat4.inverse(m);
var v0 = this._transformOnSphere(mInv, this._pts[0][0], this._pts[0][1]); //project on sphere (Xold, Yold)
var v1 = this._transformOnSphere(mInv, this._pts[1][0], this._pts[1][1]); //project on sphere (Xnew, Ynew)
var axis = SglVec3.cross(v0, v1); //axis of rotation
var angle = SglVec3.length(axis); //angle of rotation
var rotMat = SglMat4.rotationAngleAxis(angle, axis);
this._sphereMatrix = SglMat4.mul(rotMat, this._sphereMatrix);
this._computeMatrix();
},
pan : function(m) {
var mInv = SglMat4.inverse(m);
var v0 = this._transform(mInv, this._pts[0][0], this._pts[0][1], -1.0);
var v1 = this._transform(mInv, this._pts[1][0], this._pts[1][1], -1.0);
var offset = SglVec3.sub(v1, v0);
this._translate(offset, 2.0);
this._computeMatrix();
},
dolly : function(m, dz) {
var mInv = SglMat4.inverse(m);
var offset = this._transform(mInv, 0.0, 0.0, dz);
this._translate(offset, 1.0);
this._computeMatrix();
},
scale : function(m, s) {
this._distance *= s;
this._distance = this.clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
this._computeMatrix();
}
};
/***********************************************************************/

View File

@@ -0,0 +1,354 @@
/*
3DHOP - 3D Heritage Online Presenter
Copyright (c) 2014-2020, Visual Computing Lab, ISTI - CNR
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Constructs a TurntableTrackball object.
* @class Interactor which implements a Turntable controller with bounds.
*/
function TurnTableTrackball() {
}
TurnTableTrackball.prototype = {
setup : function (options) {
options = options || {};
var opt = sglGetDefaultObject({
startCenter : [ 0.0, 0.0, 0.0 ],
startPhi : 0.0,
startTheta : 0.0,
startDistance : 2.0,
minMaxDist : [0.2, 4.0],
minMaxPhi : [-180, 180],
minMaxTheta : [-80.0, 80.0],
pathStates : [ ], // path points array
animationLocked : false,// if true disable trackball interactions during animation
animationTime : null // when single position navigation, each to point navigation is # seconds (if null, automatically computed)
}, options);
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._matrix = SglMat4.identity();
// path
this._pathStates = opt.pathStates;
this._animationLocked = opt.animationLocked;
this._pathPosNum = 0;
// trackball center
this._center = opt.startCenter;
// starting/default parameters
this._startPhi = sglDegToRad(opt.startPhi); //phi (horizontal rotation)
this._startTheta = sglDegToRad(opt.startTheta); //theta (vertical rotation)
this._startDistance = opt.startDistance; //distance
// current parameters
this._phi = this._startPhi;
this._theta = this._startTheta;
this._distance = this._startDistance;
// target paramenters
this._targetPhi = this._startPhi;
this._targetTheta = this._startTheta;
this._targetDistance = this._startDistance;
//animation data
this._isAnimating = false;
this._speedPhi = 0.0;
this._speedTheta = 0.0;
this._speedDistance = 0.0;
this._isAutoWalking = false;
this._animationTime = opt.animationTime;
// limits
this._minMaxDist = opt.minMaxDist;
if((opt.minMaxPhi[0] == -180)&&(opt.minMaxPhi[1] == 180))
this._limitPhi = false;
else
this._limitPhi = true;
this._minMaxPhi = opt.minMaxPhi;
this._minMaxPhi[0] = sglDegToRad(this._minMaxPhi[0]);
this._minMaxPhi[1] = sglDegToRad(this._minMaxPhi[1]);
this._minMaxTheta = opt.minMaxTheta;
this._minMaxTheta[0] = sglDegToRad(this._minMaxTheta[0]);
this._minMaxTheta[1] = sglDegToRad(this._minMaxTheta[1]);
this._start = [0.0, 0.0];
this.reset();
},
_clamp : function(value, low, high) {
if(value < low) return low;
if(value > high) return high;
return value;
},
_computeMatrix: function() {
var m = SglMat4.identity();
// centering
m = SglMat4.mul(m, SglMat4.translation([-this._center[0], -this._center[1], -this._center[2]]));
// zoom
m = SglMat4.mul(m, SglMat4.translation([0.0, 0.0, -this._distance]));
// rotation
m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._theta, [1.0, 0.0, 0.0]));
// tilt
m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._phi, [0.0, -1.0, 0.0]));
this._matrix = m;
if(typeof onTrackballUpdate != "undefined")
onTrackballUpdate(this.getState());
},
getState : function () {
return [sglRadToDeg(this._phi), sglRadToDeg(this._theta), this._distance];
},
setState : function (newstate) {
// stop animation
this._isAnimating = this._isAutoWalking = false;
this._phi = sglDegToRad(newstate[0]);
this._theta = sglDegToRad(newstate[1]);
this._distance = newstate[2];
//check limits
if(this._limitPhi)
this._phi = this._clamp(this._phi, this._minMaxPhi[0], this._minMaxPhi[1]);
this._theta = this._clamp(this._theta, this._minMaxTheta[0], this._minMaxTheta[1]);
this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
this._computeMatrix();
},
animateToState : function (newstate, newtime) {
// stop animation
this._isAnimating = false;
if(newstate)
{
// stop autoWalking
this._isAutoWalking = false;
// setting targets
this._targetPhi = sglDegToRad(newstate[0]);
this._targetTheta = sglDegToRad(newstate[1]);
this._targetDistance = newstate[2];
//check limits
if(this._limitPhi)
this._targetPhi = this._clamp(this._targetPhi, this._minMaxPhi[0], this._minMaxPhi[1]);
this._targetPhi = this._targetPhi % (2*Math.PI);
this._targetTheta = this._clamp(this._targetTheta, this._minMaxTheta[0], this._minMaxTheta[1]);
this._targetDistance = this._clamp(this._targetDistance, this._minMaxDist[0], this._minMaxDist[1]);
// setting base velocities
this._speedPhi = Math.PI;
this._speedTheta = Math.PI;
this._speedDistance = 2.0;
//if phi unconstrained rotation, it is necessary to find a good rotation direction
if(!this._limitPhi){
// normalize (-2pi 2pi) current phi angle, to prevent endless unwinding
this._phi = this._phi % (2*Math.PI);
// determine minimal, normalized target phi angle, to prevent endless unwinding
var clampedangle = this._targetPhi;
clampedangle = clampedangle % (2*Math.PI);
if(Math.abs(clampedangle - this._phi) < Math.PI) { // standard rotation
if(clampedangle > this._phi){
this.speedphi = Math.PI;
}
else{
this.speedphi = -Math.PI;
}
}
else{
if(clampedangle > this._phi){
clampedangle = (clampedangle - 2*Math.PI)
this.speedphi = -Math.PI;
}
else{
clampedangle = (clampedangle + 2*Math.PI)
this.speedphi = Math.PI;
}
}
this._targetPhi = clampedangle;
}
// find max animation time to set a time limit and then synchronize all movements
var timePhi = Math.abs((this._targetPhi - this._phi) / this._speedPhi);
var timeTheta = Math.abs((this._targetTheta - this._theta) / this._speedTheta);
var timeDistance = Math.abs((this._targetDistance - this._distance) / this._speedDistance);
var maxtime = Math.max( timePhi, Math.max( timeTheta, timeDistance ));
var animationtime = this._clamp(maxtime, 0.5, 2.0);
if(newtime) animationtime = newtime;
else if (this._animationTime) animationtime = this._animationTime;
this._speedPhi *= timePhi / animationtime;
this._speedTheta *= timeTheta / animationtime;
this._speedDistance *= timeDistance / animationtime;
}
else
{
if(this._pathPosNum == this._pathStates.length){
this._isAutoWalking = false;
this._pathPosNum = 0;
}
else {
var state = this._pathStates[this._pathPosNum][0];
var time = this._animationTime;
if(!Array.isArray(state)) state = this._pathStates[this._pathPosNum];
else if (this._pathStates[this._pathPosNum][1]) time = this._pathStates[this._pathPosNum][1];
if(!this._isAutoWalking) this.animateToState(state, time);
this._isAutoWalking = true;
}
}
// start animation
this._isAnimating = true;
},
tick : function (dt) {
if(!this._isAnimating) return false;
var deltaPhi = this._speedPhi * dt;
var deltaTheta = this._speedTheta * dt;
var deltaDistance = this._speedDistance * dt;
var diffPhi = this._targetPhi - this._phi;
var diffTheta = this._targetTheta - this._theta;
var diffDistance = this._targetDistance - this._distance;
if (diffPhi > deltaPhi)
this._phi += deltaPhi;
else if (diffPhi < -deltaPhi)
this._phi -= deltaPhi;
else
this._phi = this._targetPhi;
if (diffTheta > deltaTheta)
this._theta += deltaTheta;
else if (diffTheta < -deltaTheta)
this._theta -= deltaTheta;
else
this._theta = this._targetTheta;
if (diffDistance > deltaDistance)
this._distance += deltaDistance;
else if (diffDistance < -deltaDistance)
this._distance -= deltaDistance;
else
this._distance = this._targetDistance;
if(this._phi == this._targetPhi)
if(this._theta == this._targetTheta)
if(this._distance == this._targetDistance){
this._isAnimating = false;
if(typeof onTrackballArrived != "undefined")
onTrackballArrived(this.getState());
if(this._isAutoWalking) { this._pathPosNum++; this._isAutoWalking = false; this.animateToState(); }
}
this._computeMatrix();
return true;
},
set action(a) { if(this._action != a) { this._new_action = true; this._action = a; } },
get action() { return this._action; },
get matrix() { this._computeMatrix(); return this._matrix; },
get distance() { return this._distance; },
reset : function () {
this._matrix = SglMat4.identity();
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._phi = this._startPhi;
this._theta = this._startTheta;
this._distance = this._startDistance;
this._pathPosNum = 0;
this._isAutoWalking = false;
this._isAnimating = false;
this._computeMatrix();
},
track : function(m, x, y, z) {
if(this._animationLocked && this._isAnimating) this._action = SGL_TRACKBALL_NO_ACTION;
if(this._new_action) {
this._start[0] = x;
this._start[1] = y;
this._new_action = false;
}
var dx = this._start[0] - x;
var dy = this._start[1] - y;
this._start[0] = x;
this._start[1] = y;
switch (this._action) {
case SGL_TRACKBALL_ROTATE:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.rotate(m, dx, dy);
break;
case SGL_TRACKBALL_PAN:
break;
case SGL_TRACKBALL_SCALE:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.scale(m, z);
break;
default:
break;
}
return this._computeMatrix();
},
rotate: function(m, dx, dy) {
this._phi += dx;
if(this._limitPhi)
this._phi = this._clamp(this._phi, this._minMaxPhi[0], this._minMaxPhi[1]);
// avoid eternal accumulation of rotation, just for the sake of cleanliness
if (this._phi > 10.0) this._phi = this._phi - 10.0;
if (this._phi < -10.0) this._phi = this._phi + 10.0;
this._theta += dy;
this._theta = this._clamp(this._theta, this._minMaxTheta[0], this._minMaxTheta[1]);
},
scale : function(m, s) {
this._distance *= s;
this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
}
};
/***********************************************************************/

View File

@@ -0,0 +1,467 @@
/*
3DHOP - 3D Heritage Online Presenter
Copyright (c) 2014-2020, Visual Computing Lab, ISTI - CNR
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Constructs a TurntableCylTrackball object.
* @class Interactor which implements a Turntable controller + cylindrical pan with bounds.
*/
function TurntablePanTrackball() {
}
TurntablePanTrackball.prototype = {
setup : function (options) {
options = options || {};
var opt = sglGetDefaultObject({
startCenter : [ 0.0, 0.0, 0.0 ],
startPhi : 0.0,
startTheta : 0.0,
startDistance : 2.0,
startPanX : 0.0,
startPanY : 0.0,
startPanZ : 0.0,
minMaxDist : [0.2, 4.0],
minMaxPhi : [-180, 180],
minMaxTheta : [-80.0, 80.0],
minMaxPanX : [-1.0, 1.0],
minMaxPanY : [-1.0, 1.0],
minMaxPanZ : [-1.0, 1.0],
pathStates : [ ], // path points array
animationLocked : false,// if true disable trackball interactions during animation
animationTime : null // when single position navigation, each to point navigation is # seconds (if null, automatically computed)
}, options);
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._matrix = SglMat4.identity();
// path
this._pathStates = opt.pathStates;
this._animationLocked = opt.animationLocked;
this._pathPosNum = 0;
// trackball center
this._center = opt.startCenter;
// starting/default parameters
this._startPhi = sglDegToRad(opt.startPhi); //phi (horizontal rotation)
this._startTheta = sglDegToRad(opt.startTheta); //theta (vertical rotation)
this._startPanX = opt.startPanX; //panX
this._startPanY = opt.startPanY; //panY
this._startPanZ = opt.startPanZ; //panZ
this._startDistance = opt.startDistance; //distance
// current parameters
this._phi = this._startPhi;
this._theta = this._startTheta;
this._panX = this._startPanX;
this._panY = this._startPanY;
this._panZ = this._startPanZ;
this._distance = this._startDistance;
// target paramenters
this._targetPhi = this._startPhi;
this._targetTheta = this._startTheta;
this._targetPanX = this._startPanX;
this._targetPanY = this._startPanY;
this._targetPanZ = this._startPanZ;
this._targetDistance = this._startDistance;
//animation data
this._isAnimating = false;
this._speedPhi = 0.0;
this._speedTheta = 0.0;
this._speedPanX = 0.0;
this._speedPanY = 0.0;
this._speedPanZ = 0.0;
this._speedDistance = 0.0;
this._isAutoWalking = false;
this._animationTime = opt.animationTime;
// limits
this._minMaxDist = opt.minMaxDist;
if((opt.minMaxPhi[0] == -180)&&(opt.minMaxPhi[1] == 180))
this._limitPhi = false;
else
this._limitPhi = true;
this._minMaxPhi = opt.minMaxPhi;
this._minMaxPhi[0] = sglDegToRad(this._minMaxPhi[0]);
this._minMaxPhi[1] = sglDegToRad(this._minMaxPhi[1]);
this._minMaxTheta = opt.minMaxTheta;
this._minMaxTheta[0] = sglDegToRad(this._minMaxTheta[0]);
this._minMaxTheta[1] = sglDegToRad(this._minMaxTheta[1]);
this._minMaxPanX = opt.minMaxPanX;
this._minMaxPanY = opt.minMaxPanY;
this._minMaxPanZ = opt.minMaxPanZ;
this._start = [0.0, 0.0];
this.reset();
},
_clamp : function(value, low, high) {
if(value < low) return low;
if(value > high) return high;
return value;
},
_computeMatrix: function() {
var m = SglMat4.identity();
// centering
m = SglMat4.mul(m, SglMat4.translation([-this._center[0], -this._center[1], -this._center[2]]));
// zoom
m = SglMat4.mul(m, SglMat4.translation([0.0, 0.0, -this._distance]));
// rotation
m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._theta, [1.0, 0.0, 0.0]));
// tilt
m = SglMat4.mul(m, SglMat4.rotationAngleAxis(this._phi, [0.0, -1.0, 0.0]));
// panning
m = SglMat4.mul(m, SglMat4.translation([-this._panX, -this._panY, -this._panZ]));
this._matrix = m;
if(typeof onTrackballUpdate != "undefined")
onTrackballUpdate(this.getState());
},
getState : function () {
return [sglRadToDeg(this._phi), sglRadToDeg(this._theta), this._panX, this._panY, this._panZ, this._distance];
},
setState : function (newstate) {
// stop animation
this._isAnimating = this._isAutoWalking = false;
this._phi = sglDegToRad(newstate[0]);
this._theta = sglDegToRad(newstate[1]);
this._panX = newstate[2];
this._panY = newstate[3];
this._panZ = newstate[4];
this._distance = newstate[5];
//check limits
if(this._limitPhi)
this._phi = this._clamp(this._phi, this._minMaxPhi[0], this._minMaxPhi[1]);
this._theta = this._clamp(this._theta, this._minMaxTheta[0], this._minMaxTheta[1]);
this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
this._panX = this._clamp(this._panX, this._minMaxPanX[0], this._minMaxPanX[1]);
this._panY = this._clamp(this._panY, this._minMaxPanY[0], this._minMaxPanY[1]);
this._panZ = this._clamp(this._panZ, this._minMaxPanZ[0], this._minMaxPanZ[1]);
this._computeMatrix();
},
animateToState : function (newstate, newtime) {
// stop animation
this._isAnimating = false;
if(newstate)
{
// stop autoWalking
this._isAutoWalking = false;
// setting targets
this._targetPhi = sglDegToRad(newstate[0]);
this._targetTheta = sglDegToRad(newstate[1]);
this._targetPanX = newstate[2];
this._targetPanY = newstate[3];
this._targetPanZ = newstate[4];
this._targetDistance = newstate[5];
//check limits
if(this._limitPhi)
this._targetPhi = this._clamp(this._targetPhi, this._minMaxPhi[0], this._minMaxPhi[1]);
this._targetPhi = this._targetPhi % (2*Math.PI);
this._targetTheta = this._clamp(this._targetTheta, this._minMaxTheta[0], this._minMaxTheta[1]);
this._targetPanX = this._clamp(this._targetPanX, this._minMaxPanX[0], this._minMaxPanX[1]);
this._targetPanY = this._clamp(this._targetPanY, this._minMaxPanY[0], this._minMaxPanY[1]);
this._targetPanZ = this._clamp(this._targetPanZ, this._minMaxPanZ[0], this._minMaxPanZ[1]);
this._targetDistance = this._clamp(this._targetDistance, this._minMaxDist[0], this._minMaxDist[1]);
// setting base velocities
this._speedPhi = Math.PI;
this._speedTheta = Math.PI;
this._speedPanX = 1.0;
this._speedPanY = 1.0;
this._speedPanZ = 1.0;
this._speedDistance = 2.0;
//if phi unconstrained rotation, it is necessary to find a good rotation direction
if(!this._limitPhi){
// normalize (-2pi 2pi) current phi angle, to prevent endless unwinding
this._phi = this._phi % (2*Math.PI);
// determine minimal, normalized target phi angle, to prevent endless unwinding
var clampedangle = this._targetPhi;
clampedangle = clampedangle % (2*Math.PI);
if(Math.abs(clampedangle - this._phi) < Math.PI) { // standard rotation
if(clampedangle > this._phi){
this.speedphi = Math.PI;
}
else{
this.speedphi = -Math.PI;
}
}
else{
if(clampedangle > this._phi){
clampedangle = (clampedangle - 2*Math.PI)
this.speedphi = -Math.PI;
}
else{
clampedangle = (clampedangle + 2*Math.PI)
this.speedphi = Math.PI;
}
}
this._targetPhi = clampedangle;
}
// find max animation time to set a time limit and then synchronize all movements
var timePhi = Math.abs((this._targetPhi - this._phi) / this._speedPhi);
var timeTheta = Math.abs((this._targetTheta - this._theta) / this._speedTheta);
var timeDistance = Math.abs((this._targetDistance - this._distance) / this._speedDistance);
var timePanX = Math.abs((this._targetPanX - this._panX) / this._speedPanX);
var timePanY = Math.abs((this._targetPanY - this._panY) / this._speedPanY);
var timePanZ = Math.abs((this._targetPanZ - this._panZ) / this._speedPanZ);
var maxtime = Math.max( timePhi, Math.max( timeTheta, Math.max( timeDistance, Math.max( timePanX, Math.max( timePanY, timePanZ )))));
var animationtime = this._clamp(maxtime, 0.5, 2.0);
if(newtime) animationtime = newtime;
else if (this._animationTime) animationtime = this._animationTime;
this._speedPhi *= timePhi / animationtime;
this._speedTheta *= timeTheta / animationtime;
this._speedDistance *= timeDistance / animationtime;
this._speedPanX *= timePanX / animationtime;
this._speedPanY *= timePanY / animationtime;
this._speedPanZ *= timePanZ / animationtime;
}
else
{
if(this._pathPosNum == this._pathStates.length){
this._isAutoWalking = false;
this._pathPosNum = 0;
}
else {
var state = this._pathStates[this._pathPosNum][0];
var time = this._animationTime;
if(!Array.isArray(state)) state = this._pathStates[this._pathPosNum];
else if (this._pathStates[this._pathPosNum][1]) time = this._pathStates[this._pathPosNum][1];
if(!this._isAutoWalking) this.animateToState(state, time);
this._isAutoWalking = true;
}
}
// start animation
this._isAnimating = true;
},
recenter : function (newpoint) {
// stop animation
this._isAnimating = this._isAutoWalking = false;
var newpanX = (newpoint[0]-presenter.sceneCenter[0]) * presenter.sceneRadiusInv;
var newpanY = (newpoint[1]-presenter.sceneCenter[1]) * presenter.sceneRadiusInv;
var newpanZ = (newpoint[2]-presenter.sceneCenter[2]) * presenter.sceneRadiusInv;
this.animateToState([sglRadToDeg(this._phi), sglRadToDeg(this._theta), newpanX, newpanY, newpanZ, (this._distance * 0.6)]);
},
tick : function (dt) {
if(!this._isAnimating) return false;
var deltaPhi = this._speedPhi * dt;
var deltaTheta = this._speedTheta * dt;
var deltaDistance = this._speedDistance * dt;
var deltaPanX = this._speedPanX * dt;
var deltaPanY = this._speedPanY * dt;
var deltaPanZ = this._speedPanZ * dt;
var diffPhi = this._targetPhi - this._phi;
var diffTheta = this._targetTheta - this._theta;
var diffDistance = this._targetDistance - this._distance;
var diffPanX = this._targetPanX - this._panX;
var diffPanY = this._targetPanY - this._panY;
var diffPanZ = this._targetPanZ - this._panZ;
if (diffPhi > deltaPhi)
this._phi += deltaPhi;
else if (diffPhi < -deltaPhi)
this._phi -= deltaPhi;
else
this._phi = this._targetPhi;
if (diffTheta > deltaTheta)
this._theta += deltaTheta;
else if (diffTheta < -deltaTheta)
this._theta -= deltaTheta;
else
this._theta = this._targetTheta;
if (diffDistance > deltaDistance)
this._distance += deltaDistance;
else if (diffDistance < -deltaDistance)
this._distance -= deltaDistance;
else
this._distance = this._targetDistance;
if (diffPanX > deltaPanX)
this._panX += deltaPanX;
else if (diffPanX < -deltaPanX)
this._panX -= deltaPanX;
else
this._panX = this._targetPanX;
if (diffPanY > deltaPanY)
this._panY += deltaPanY;
else if (diffPanY < -deltaPanY)
this._panY -= deltaPanY;
else
this._panY = this._targetPanY;
if (diffPanZ > deltaPanZ)
this._panZ += deltaPanZ;
else if (diffPanZ < -deltaPanZ)
this._panZ -= deltaPanZ;
else
this._panZ = this._targetPanZ;
if(this._phi == this._targetPhi)
if(this._theta == this._targetTheta)
if(this._distance == this._targetDistance)
if(this._panX == this._targetPanX)
if(this._panY == this._targetPanY)
if(this._panZ == this._targetPanZ){
this._isAnimating = false;
if(typeof onTrackballArrived != "undefined")
onTrackballArrived(this.getState());
if(this._isAutoWalking) { this._pathPosNum++; this._isAutoWalking = false; this.animateToState(); }
}
this._computeMatrix();
return true;
},
set action(a) { if(this._action != a) { this._new_action = true; this._action = a; } },
get action() { return this._action; },
get matrix() { this._computeMatrix(); return this._matrix; },
get distance() { return this._distance; },
reset : function () {
this._matrix = SglMat4.identity();
this._action = SGL_TRACKBALL_NO_ACTION;
this._new_action = true;
this._phi = this._startPhi;
this._theta = this._startTheta;
this._distance = this._startDistance;
this._panX = this._startPanX;
this._panY = this._startPanY;
this._panZ = this._startPanZ;
this._pathPosNum = 0;
this._isAutoWalking = false;
this._isAnimating = false;
this._computeMatrix();
},
track : function(m, x, y, z) {
if(this._animationLocked && this._isAnimating) this._action = SGL_TRACKBALL_NO_ACTION;
if(this._new_action) {
this._start[0] = x;
this._start[1] = y;
this._new_action = false;
}
var dx = this._start[0] - x;
var dy = this._start[1] - y;
this._start[0] = x;
this._start[1] = y;
switch (this._action) {
case SGL_TRACKBALL_ROTATE:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.rotate(m, dx, dy);
break;
case SGL_TRACKBALL_PAN:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.pan(m, dx, dy);
break;
case SGL_TRACKBALL_SCALE:
this._isAnimating = this._isAutoWalking = false; //stopping animation
this.scale(m, z);
break;
default:
break;
}
return this._computeMatrix();
},
pan: function(m, dx, dy) {
//determining current X, Y and Z axis
var Xvec = [1.0, 0.0, 0.0, 1.0];
var Yvec = [0.0, 1.0, 0.0, 1.0];
var Zvec = [0.0, 0.0, 1.0, 1.0];
Xvec = SglMat4.mul4(SglMat4.rotationAngleAxis(this._phi, [0.0, -1.0, 0.0]), Xvec);
Yvec = SglMat4.mul4(SglMat4.rotationAngleAxis(this._phi, [0.0, -1.0, 0.0]), Yvec);
Zvec = SglMat4.mul4(SglMat4.rotationAngleAxis(this._phi, [0.0, -1.0, 0.0]), Zvec);
Xvec = SglMat4.mul4(SglMat4.rotationAngleAxis(this._theta, [1.0, 0.0, 0.0]), Xvec);
Yvec = SglMat4.mul4(SglMat4.rotationAngleAxis(this._theta, [1.0, 0.0, 0.0]), Yvec);
Zvec = SglMat4.mul4(SglMat4.rotationAngleAxis(this._theta, [1.0, 0.0, 0.0]), Zvec);
var panSpeed = Math.max(Math.min(1.5, this._distance),0.05);
this._panX += ((dx * Xvec[0]) + (dy * Xvec[1])) * panSpeed;
this._panY += ((dx * Yvec[0]) + (dy * Yvec[1])) * panSpeed;
this._panZ += ((dx * Zvec[0]) + (dy * Zvec[1])) * panSpeed;
//clamping
this._panX = this._clamp(this._panX, this._minMaxPanX[0], this._minMaxPanX[1]);
this._panY = this._clamp(this._panY, this._minMaxPanY[0], this._minMaxPanY[1]);
this._panZ = this._clamp(this._panZ, this._minMaxPanZ[0], this._minMaxPanZ[1]);
},
rotate: function(m, dx, dy) {
this._phi += dx;
if(this._limitPhi)
this._phi = this._clamp(this._phi, this._minMaxPhi[0], this._minMaxPhi[1]);
// avoid eternal accumulation of rotation, just for the sake of cleanliness
if (this._phi > 10.0) this._phi = this._phi - 10.0;
if (this._phi < -10.0) this._phi = this._phi + 10.0;
this._theta += dy;
this._theta = this._clamp(this._theta, this._minMaxTheta[0], this._minMaxTheta[1]);
},
scale : function(m, s) {
this._distance *= s;
this._distance = this._clamp(this._distance, this._minMaxDist[0], this._minMaxDist[1]);
}
};
/***********************************************************************/

View File

@@ -0,0 +1,23 @@
import "@/styles/main.css"
type BootstrapOptions = {
activeLink?: string | null;
onReady?: () => void | Promise<void>;
};
export async function bootstrap(options: BootstrapOptions = {}): Promise<void> {
const {
activeLink = null,
onReady,
} = options;
if(activeLink !== null){setActiveLink(activeLink);}
await onReady?.();
}
function setActiveLink(activeLink:string): void{
const link = document.getElementById(activeLink);
link?.classList.add('active');
}

View File

View File

@@ -0,0 +1,9 @@
import { bootstrap } from "@/config/bootstrap"
bootstrap({
onReady:() => init()
})
export function init(){
console.log('index')
}

View File

@@ -0,0 +1,8 @@
@import "tailwindcss";
@plugin "daisyui"; /* NOSONAR */
:root{
--dc-primary: rgb(34, 69, 138);
--dc-dark-blue: rgb(0, 15, 46);
--dc-white: rgb(255, 255, 255);
}

31
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client", "node", "leaflet"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* Path Aliases - Molto utile per evitare ../../../ */
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

106
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,106 @@
import { defineConfig } from 'vitest/config'
import type { Plugin } from 'vite'
import { fileURLToPath } from 'node:url'
import { readdirSync } from 'node:fs'
import tailwindcss from '@tailwindcss/vite'
import { visualizer } from 'rollup-plugin-visualizer'
// MPA: raccoglie automaticamente ogni *.html nella root di frontend/ come entry di build.
// { 'index': '/app/index.html', 'scheda': '/app/scheda.html', ... }
const frontendRoot = fileURLToPath(new URL('.', import.meta.url))
const htmlEntries = Object.fromEntries(
readdirSync(frontendRoot)
.filter((file) => file.endsWith('.html'))
.map((file) => [file.slice(0, -'.html'.length), fileURLToPath(new URL(file, import.meta.url))])
)
function mpaRewritePlugin(): Plugin {
return {
name: 'mpa-rewrite',
configureServer(server: any) {
server.middlewares.use((req: any, _res: any, next: any) => {
if (
req.url &&
!req.url.includes('.') &&
!req.url.startsWith('/@') &&
!req.url.startsWith('/api') &&
!req.url.startsWith('/sanctum') &&
!req.url.startsWith('/storage') &&
!req.url.startsWith('/documentation') &&
!req.url.startsWith('/api-docs') &&
req.url !== '/'
) {
// Mappa il primo segmento del path sul relativo .html.
// Es: /scheda/uno-slug -> /scheda.html (lo slug resta leggibile da location.pathname)
const firstSegment = req.url.split('?')[0].split('/')[1]
req.url = `/${firstSegment}.html`
}
next()
})
}
}
}
export default defineConfig({
plugins: [
tailwindcss(),
visualizer(),
mpaRewritePlugin()
],
resolve: {
// Alias lato Vite/bundler: tsconfig `paths` vale solo per il type-check, Vite
// ha bisogno del proprio alias per risolvere `@/...` a runtime e in build.
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
build: {
rollupOptions: {
// MPA: auto-detect delle pagine. Ogni *.html nella root di frontend/ è un entry
// (nome = file senza estensione). Aggiungere una pagina = creare il suo .html,
// nessuna modifica qui. Vedi htmlEntries sotto.
input: htmlEntries,
}
},
server: {
host: '0.0.0.0',
allowedHosts: ['dyncoll-dev.local'],
port: 5173,
proxy: {
'/api-docs': {
target: 'http://backend:8000',
changeOrigin: true,
secure: false,
rewrite: (path: string) => path.replace(/^\/api-docs/, '/docs')
},
'/api': {
target: 'http://backend:8000',
changeOrigin: true,
secure: false // accetta certificati self-signed
},
'/sanctum': {
target: 'http://backend:8000',
changeOrigin: true,
secure: false,
},
'/storage': {
target: 'http://backend:8000',
changeOrigin: true,
secure: false // accetta certificati self-signed
},
// Documentazione utente (MkDocs) — servizio docker `docs`
'/documentation': {
target: 'http://docs:8000',
changeOrigin: true,
secure: false,
rewrite: (path: string) => path.replace(/^\/documentation/, '') || '/'
}
}
},
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
}
})