Aggiunge --3d: genera un file HTML che si apre con un doppio clic e permette di ruotare il pezzo col mouse, ingrandire con la rotella e spostare col tasto destro. Mesh, stile e codice sono dentro la pagina: nessuna libreria esterna e nessuna connessione, cosi' funziona anche da una cartella di rete. Il disegno usa una canvas 2D con ordinamento per profondita' invece di WebGL, che su alcune postazioni e' disattivato. La forma non viene dal .prt ma da una mesh STL esportata da Creo, cercata accanto al modello per nome (9258400207.prt.11 -> 9258400207.stl). Se manca, il comando lo dice invece di fallire. Perche' serve la mesh: la geometria nel .prt e' scritta nel formato del kernel Granite di PTC ed e' compressa. Verificato con un attacco a testo noto, presi da un export STEP i 22 valori esatti del modello (raggi 1,6 / 2,1 / 2,5 / 2,75 / 5,25 e coordinate fino a 75,0) e cercati nel file in cinque codifiche su tutti i blocchi: nessun riscontro. La via ufficiale per leggere i .prt nativi senza Creo e' il Granite Interoperability Kernel di PTC, che si innesterebbe al posto di mesh.py senza toccare il resto. Quando la mesh c'e', il record guadagna un blocco "geometry" con ingombro, volume e area. Il peso NON viene calcolato: servirebbe la densita', che dipende dal materiale, e il materiale e' testo libero. Sul 9258400207 il parametro dice "Anticordal 100", che e' alluminio: applicare la densita' dell'acciaio darebbe 0,686 kg invece di 0,237. Gli export (stl, dxf, dwg, iges, pdf) sono esclusi dal repository come i modelli: sono geometria e tavole complete dei pezzi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
187 lines
6.6 KiB
Python
187 lines
6.6 KiB
Python
"""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' (painter's
|
|
algorithm): per le mesh di un componente meccanico e' abbondante e non
|
|
richiede WebGL, che su alcune postazioni aziendali e' disattivato.
|
|
"""
|
|
|
|
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:#f4f5f7;color:#1a1c1e;display:flex;flex-direction:column;
|
|
height:100vh;overflow:hidden}
|
|
header{padding:.7rem 1rem;background:#fff;border-bottom:1px solid #d8dbe0;
|
|
display:flex;gap:1.2rem;align-items:baseline;flex-wrap:wrap}
|
|
h1{font-size:1rem;margin:0;font-weight:600}
|
|
.meta{color:#5c6370;font-size:.82rem}
|
|
.meta b{color:#1a1c1e;font-weight:600}
|
|
#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 #d8dbe0;
|
|
color:#5c6370;font-size:.78rem;display:flex;gap:1rem;flex-wrap:wrap}
|
|
button{font:inherit;padding:.25rem .7rem;border:1px solid #c3c7ce;
|
|
border-radius:5px;background:#fff;color:inherit;cursor:pointer}
|
|
button:hover{background:#eef0f3}
|
|
@media (prefers-color-scheme:dark){
|
|
body{background:#16181c;color:#e6e8ea}
|
|
header,footer{background:#1e2126;border-color:#31353c}
|
|
.meta{color:#9aa1ab} .meta b{color:#e6e8ea}
|
|
button{background:#252930;border-color:#3a3f47;color:#e6e8ea}
|
|
button:hover{background:#2e333b}
|
|
}
|
|
</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>Trascina per ruotare · rotella per ingrandire · tasto destro per spostare</span>
|
|
<button id="reset">Vista iniziale</button>
|
|
<button id="mode">Contorni</button>
|
|
</footer>
|
|
<script>
|
|
const TRI = %(mesh)s;
|
|
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
|
|
let rx = -1.05, ry = 0.62, zoom = 1, panx = 0, pany = 0, edges = false;
|
|
|
|
// baricentro e raggio, per inquadrare il pezzo qualunque sia la sua scala
|
|
let cx=0, cy=0, cz=0, n=0;
|
|
for (const t of TRI) for (let k=0;k<9;k+=3){ cx+=t[k]; cy+=t[k+1]; cz+=t[k+2]; n++; }
|
|
cx/=n; cy/=n; cz/=n;
|
|
let radius = 1e-9;
|
|
for (const t of TRI) for (let k=0;k<9;k+=3){
|
|
const d = Math.hypot(t[k]-cx, t[k+1]-cy, t[k+2]-cz);
|
|
if (d > radius) radius = d;
|
|
}
|
|
|
|
function resize(){
|
|
const r = cv.parentElement.getBoundingClientRect();
|
|
const dpr = Math.min(window.devicePixelRatio||1, 2);
|
|
cv.width = r.width*dpr; cv.height = r.height*dpr;
|
|
ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
draw();
|
|
}
|
|
|
|
function draw(){
|
|
const w = cv.width/(window.devicePixelRatio||1), h = cv.height/(window.devicePixelRatio||1);
|
|
ctx.clearRect(0,0,w,h);
|
|
const dark = matchMedia('(prefers-color-scheme: dark)').matches;
|
|
const scale = Math.min(w,h)/(2.4*radius)*zoom;
|
|
const ox = w/2 + panx, oy = h/2 + pany;
|
|
const sx=Math.sin(rx), cxr=Math.cos(rx), sy=Math.sin(ry), cyr=Math.cos(ry);
|
|
|
|
const faces = [];
|
|
for (const t of TRI){
|
|
const p = [];
|
|
for (let k=0;k<9;k+=3){
|
|
let x=t[k]-cx, y=t[k+1]-cy, z=t[k+2]-cz;
|
|
let X = x*cyr + z*sy, Z = -x*sy + z*cyr;
|
|
let Y = y*cxr - Z*sx; Z = y*sx + Z*cxr;
|
|
p.push([ox + X*scale, oy - Y*scale, Z]);
|
|
}
|
|
// normale in coordinate schermo: da' l'illuminazione e scarta il retro
|
|
const ux=p[1][0]-p[0][0], uy=p[1][1]-p[0][1], uz=p[1][2]-p[0][2];
|
|
const vx=p[2][0]-p[0][0], vy=p[2][1]-p[0][1], vz=p[2][2]-p[0][2];
|
|
const nx=uy*vz-uz*vy, ny=uz*vx-ux*vz, nz=ux*vy-uy*vx;
|
|
const len = Math.hypot(nx,ny,nz) || 1;
|
|
faces.push({p, depth:(p[0][2]+p[1][2]+p[2][2])/3, light: Math.abs(nz/len)});
|
|
}
|
|
faces.sort((a,b)=>a.depth-b.depth); // i piu' lontani per primi
|
|
|
|
ctx.lineJoin = 'round';
|
|
for (const f of faces){
|
|
const v = dark ? 40 + f.light*150 : 120 + f.light*125;
|
|
ctx.beginPath();
|
|
ctx.moveTo(f.p[0][0], f.p[0][1]);
|
|
ctx.lineTo(f.p[1][0], f.p[1][1]);
|
|
ctx.lineTo(f.p[2][0], f.p[2][1]);
|
|
ctx.closePath();
|
|
if (edges){
|
|
ctx.strokeStyle = dark ? '#8ab4ff' : '#2b4c7e';
|
|
ctx.lineWidth = 0.6; ctx.stroke();
|
|
} else {
|
|
const c = Math.round(v);
|
|
ctx.fillStyle = `rgb(${c},${c},${Math.min(255,c+6)})`;
|
|
ctx.fill();
|
|
ctx.strokeStyle = ctx.fillStyle; ctx.lineWidth = 0.5; 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 stop = e => { drag = null; cv.classList.remove('drag'); };
|
|
cv.addEventListener('pointerup', stop);
|
|
cv.addEventListener('pointercancel', stop);
|
|
cv.addEventListener('contextmenu', e => e.preventDefault());
|
|
cv.addEventListener('wheel', e => {
|
|
e.preventDefault();
|
|
zoom *= e.deltaY < 0 ? 1.12 : 1/1.12;
|
|
zoom = Math.max(0.15, Math.min(zoom, 40));
|
|
draw();
|
|
}, {passive:false});
|
|
|
|
document.getElementById('reset').onclick = () => {
|
|
rx=-1.05; ry=0.62; zoom=1; panx=0; pany=0; draw();
|
|
};
|
|
document.getElementById('mode').onclick = e => {
|
|
edges = !edges;
|
|
e.target.textContent = edges ? 'Superfici' : 'Contorni';
|
|
draw();
|
|
};
|
|
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', draw);
|
|
addEventListener('resize', resize);
|
|
resize();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def build(mesh, title: str, subtitle: str = "") -> str:
|
|
"""Pagina HTML autonoma con il modello incorporato."""
|
|
flat = [[round(v, 4) for vertex in t for v in vertex] for t in mesh.triangles]
|
|
return TEMPLATE % {
|
|
"title": escape(title),
|
|
"subtitle": escape(subtitle),
|
|
"mesh": json.dumps(flat, separators=(",", ":")),
|
|
}
|
|
|
|
|
|
def describe(mesh, unit: str = "mm") -> str:
|
|
"""Riga di riepilogo con ingombro e numero di triangoli."""
|
|
w, d, h = mesh.size
|
|
return ("%d triangoli · ingombro %g × %g × %g %s"
|
|
% (len(mesh), w, d, h, unit))
|