Files

242 lines
8.6 KiB
Python
Raw Permalink Normal View History

"""Generazione di una pagina HTML per ruotare il modello nel browser.
La pagina e' autonoma: mesh, stile e codice sono dentro il file. Nessuna
libreria esterna, nessuna connessione. Si apre con un doppio clic, anche da
una cartella di rete, e funziona su qualunque browser recente.
Il disegno usa una canvas 2D con ordinamento per profondita' invece di WebGL,
che su alcune postazioni aziendali e' disattivato: per la mesh di un
componente meccanico e' abbondante.
Un pezzo meccanico ombreggiato e basta risulta illeggibile, perche' facce
complanari hanno tutte lo stesso colore. Qui sopra alle facce disegniamo:
- gli **spigoli vivi**, dove due facce formano un angolo netto;
- la **silhouette**, dove una faccia visibile ne incontra una nascosta.
Sono le stesse linee che un disegnatore traccerebbe a mano, e sono cio' che
rende riconoscibile la forma.
"""
from __future__ import annotations
import json
from xml.sax.saxutils import escape
TEMPLATE = """<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%(title)s</title>
<style>
:root{color-scheme:light dark}
*{box-sizing:border-box}
body{margin:0;font:14px system-ui,-apple-system,Segoe UI,sans-serif;
background:#eceef1;color:#16181c;display:flex;flex-direction:column;
height:100vh;overflow:hidden}
header{padding:.7rem 1rem;background:#fff;border-bottom:1px solid #d2d6dc;
display:flex;gap:1.1rem;align-items:baseline;flex-wrap:wrap}
h1{font-size:1rem;margin:0;font-weight:600;letter-spacing:.01em}
.meta{color:#5a616b;font-size:.82rem}
#stage{flex:1;position:relative;min-height:0}
canvas{display:block;width:100%%;height:100%%;cursor:grab;touch-action:none}
canvas.drag{cursor:grabbing}
footer{padding:.5rem 1rem;background:#fff;border-top:1px solid #d2d6dc;
color:#5a616b;font-size:.78rem;display:flex;gap:.6rem;
align-items:center;flex-wrap:wrap}
footer .hint{margin-right:auto}
button{font:inherit;padding:.28rem .7rem;border:1px solid #c2c7ce;
border-radius:5px;background:#fff;color:inherit;cursor:pointer}
button:hover{background:#eef0f3}
button[aria-pressed="true"]{background:#1f5fa8;border-color:#1f5fa8;color:#fff}
@media (prefers-color-scheme:dark){
body{background:#0f1114;color:#e8eaed}
header,footer{background:#181b20;border-color:#2c313a}
.meta{color:#98a0ab}
button{background:#20242b;border-color:#353b45;color:#e8eaed}
button:hover{background:#2a2f38}
button[aria-pressed="true"]{background:#4c8ee0;border-color:#4c8ee0;color:#0f1114}
}
</style>
</head>
<body>
<header>
<h1>%(title)s</h1>
<span class="meta">%(subtitle)s</span>
</header>
<div id="stage"><canvas id="c"></canvas></div>
<footer>
<span class="hint">Trascina per ruotare &middot; rotella per ingrandire
&middot; tasto destro per spostare</span>
<button id="shaded" aria-pressed="true">Ombreggiato</button>
<button id="wire" aria-pressed="false">Wireframe</button>
<button id="reset">Vista iniziale</button>
</footer>
<script>
const V = %(vertices)s, F = %(faces)s, E = %(edges)s;
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
let rx = -1.02, ry = 0.66, zoom = 1, panx = 0, pany = 0;
let shaded = true, wire = false;
let cx=0, cy=0, cz=0;
for (const v of V){ cx+=v[0]; cy+=v[1]; cz+=v[2]; }
cx/=V.length; cy/=V.length; cz/=V.length;
let radius = 1e-9;
for (const v of V){
const d = Math.hypot(v[0]-cx, v[1]-cy, v[2]-cz);
if (d > radius) radius = d;
}
const P = new Float64Array(V.length*3); // vertici proiettati
const NZ = new Float64Array(F.length); // orientamento delle facce
const LIT = new Float64Array(F.length); // illuminazione
const DEPTH = new Float64Array(F.length);
const order = Array.from(F.keys());
function project(){
const sx=Math.sin(rx), cxr=Math.cos(rx), sy=Math.sin(ry), cyr=Math.cos(ry);
const w = cv.clientWidth, h = cv.clientHeight;
const scale = Math.min(w,h)/(2.35*radius)*zoom;
const ox = w/2 + panx, oy = h/2 + pany;
for (let i=0;i<V.length;i++){
const v = V[i];
let x=v[0]-cx, y=v[1]-cy, z=v[2]-cz;
let X = x*cyr + z*sy; let Z = -x*sy + z*cyr;
const Y = y*cxr - Z*sx; Z = y*sx + Z*cxr;
P[i*3] = ox + X*scale; P[i*3+1] = oy - Y*scale; P[i*3+2] = Z*scale;
}
// luce obliqua da alto-sinistra: da' rilievo anche alle facce frontali
const lx=-0.42, ly=-0.58, lz=0.70;
for (let f=0;f<F.length;f++){
const [a,b,c] = F[f];
const ax=P[a*3], ay=P[a*3+1], az=P[a*3+2];
const ux=P[b*3]-ax, uy=P[b*3+1]-ay, uz=P[b*3+2]-az;
const vx=P[c*3]-ax, vy=P[c*3+1]-ay, vz=P[c*3+2]-az;
let nx=uy*vz-uz*vy, ny=uz*vx-ux*vz, nz=ux*vy-uy*vx;
const len = Math.hypot(nx,ny,nz) || 1;
nx/=len; ny/=len; nz/=len;
NZ[f] = nz;
LIT[f] = Math.abs(nx*lx + ny*ly + nz*lz);
DEPTH[f] = (az + P[b*3+2] + P[c*3+2])/3;
}
order.sort((p,q)=>DEPTH[p]-DEPTH[q]);
}
function draw(){
const dpr = Math.min(devicePixelRatio||1, 2);
const w = cv.clientWidth, h = cv.clientHeight;
cv.width = w*dpr; cv.height = h*dpr;
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.clearRect(0,0,w,h);
project();
const dark = matchMedia('(prefers-color-scheme: dark)').matches;
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
if (shaded){
for (const f of order){
if (NZ[f] < 0) continue; // faccia rivolta indietro
const [a,b,c] = F[f];
const t = LIT[f];
// rampa ampia: il contrasto e' quello che fa leggere la forma
const v = dark ? 26 + t*126 : 96 + t*150;
const g = Math.round(v);
ctx.fillStyle = `rgb(${g},${g},${Math.min(255,Math.round(g*1.03+4))})`;
ctx.beginPath();
ctx.moveTo(P[a*3],P[a*3+1]);
ctx.lineTo(P[b*3],P[b*3+1]);
ctx.lineTo(P[c*3],P[c*3+1]);
ctx.closePath(); ctx.fill();
}
}
// Spigoli: quelli vivi e la silhouette. Sono le linee del disegno tecnico.
ctx.strokeStyle = dark ? '#f2f4f7' : '#101317';
ctx.lineWidth = 1.1;
ctx.beginPath();
for (const e of E){
const [a,b,f0,f1] = e;
let show;
if (wire){
show = true;
} else if (f1 < 0){
show = true; // bordo libero
} else {
const front0 = NZ[f0] >= 0, front1 = NZ[f1] >= 0;
if (front0 !== front1) show = true; // silhouette
else if (!front0) show = false; // spigolo nascosto
else {
// spigolo vivo: le due facce formano un angolo netto
show = Math.abs(NZ[f0]-NZ[f1]) > 0.12;
}
}
if (!show) continue;
ctx.moveTo(P[a*3],P[a*3+1]);
ctx.lineTo(P[b*3],P[b*3+1]);
}
ctx.stroke();
}
let drag = null;
cv.addEventListener('pointerdown', e => {
drag = {x:e.clientX, y:e.clientY, pan:(e.button===2)};
cv.setPointerCapture(e.pointerId); cv.classList.add('drag');
});
cv.addEventListener('pointermove', e => {
if (!drag) return;
const dx = e.clientX-drag.x, dy = e.clientY-drag.y;
if (drag.pan){ panx += dx; pany += dy; } else { ry += dx*0.01; rx += dy*0.01; }
drag.x = e.clientX; drag.y = e.clientY;
draw();
});
const release = () => { drag = null; cv.classList.remove('drag'); };
cv.addEventListener('pointerup', release);
cv.addEventListener('pointercancel', release);
cv.addEventListener('contextmenu', e => e.preventDefault());
cv.addEventListener('wheel', e => {
e.preventDefault();
zoom = Math.max(0.15, Math.min(zoom * (e.deltaY < 0 ? 1.12 : 1/1.12), 40));
draw();
}, {passive:false});
const bShaded = document.getElementById('shaded');
const bWire = document.getElementById('wire');
function sync(){
bShaded.setAttribute('aria-pressed', shaded);
bWire.setAttribute('aria-pressed', wire);
draw();
}
bShaded.onclick = () => { shaded = !shaded; if (!shaded) wire = wire || false; sync(); };
bWire.onclick = () => { wire = !wire; sync(); };
document.getElementById('reset').onclick = () => {
rx=-1.02; ry=0.66; zoom=1; panx=0; pany=0; shaded=true; wire=false; sync();
};
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', draw);
addEventListener('resize', draw);
draw();
</script>
</body>
</html>
"""
def build(mesh, title: str, subtitle: str = "") -> str:
"""Pagina HTML autonoma con il modello incorporato."""
vertices, faces, edges = mesh.indexed()
compact = lambda data: json.dumps(data, separators=(",", ":"))
return TEMPLATE % {
"title": escape(title),
"subtitle": escape(subtitle),
"vertices": compact(vertices),
"faces": compact(faces),
"edges": compact(edges),
}
def describe(mesh, unit: str = "mm") -> str:
"""Riga di riepilogo con ingombro e numero di triangoli."""
w, d, h = mesh.size
return ("%d triangoli &middot; ingombro %g &times; %g &times; %g %s"
% (len(mesh), w, d, h, unit))