Files
davideandClaude Opus 5 0410266334 Vista 3D del modello in una pagina HTML autonoma
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>
2026-08-06 16:04:46 +02:00

293 lines
12 KiB
Python

"""Interfaccia a riga di comando."""
from __future__ import annotations
import argparse
import csv
import json
import os
import sys
from . import ugc
from .extract import extract as extract_file
from .extract import (find_mesh, latest_versions, read_mesh,
read_preview, split_filename)
CREO_EXTENSIONS = (".prt", ".asm", ".drw", ".frm", ".lay")
#: Parametri aziendali attesi: se mancano vengono riportati esplicitamente
#: come "parameter_not_present" invece di sparire dall'output.
DEFAULT_EXPECTED = [
"CODICE",
"DENOMINAZIONE",
"REVISIONE",
"STAMPIGLIATURA",
"MATERIALE",
"DISEGNATORE",
"DATA",
]
def _is_creo_file(path: str) -> bool:
stem, ext, _ = split_filename(os.path.basename(path))
return f".{ext}" in CREO_EXTENSIONS
def collect(inputs: "list[str]", recursive: bool) -> "tuple[list[str], list[str]]":
"""Separa i file indicati esplicitamente da quelli trovati esplorando una
cartella: solo su questi ultimi ha senso scegliere l'ultima versione."""
explicit, discovered = [], []
for item in inputs:
if os.path.isdir(item):
walker = os.walk(item) if recursive else [(item, [], os.listdir(item))]
for root, _, files in walker:
discovered.extend(
os.path.join(root, f) for f in files if _is_creo_file(f)
)
else:
explicit.append(item)
return sorted(set(explicit)), sorted(set(discovered))
def _visible(record: dict, include_system: bool, include_features: bool = False) -> dict:
return {
name: entry
for name, entry in record["parameters"].items()
if (include_system or entry.get("scope") != "system")
and (include_features or entry.get("owner", "model") == "model")
}
def _print_table(records: "list[dict]", stream, include_system: bool = False,
include_features: bool = False, limit_texts: int = 20) -> None:
for record in records:
model = record["model"]
header = f"{record['file']['name']} [{model['kind']}"
if model["subtype"]:
header += f"/{model['subtype']}"
header += f"] Creo {model['creo_version'] or '?'}"
shot = record.get("preview", {})
if shot.get("present"):
header += f" anteprima {shot['width']}x{shot['height']}"
print(header, file=stream)
print("-" * len(header), file=stream)
if not record["extraction"]["holds_model_parameters"]:
sheet = record.get("drawing")
if sheet and sheet["texts"]:
print(" %d testi nella tavola (%d unici). I parametri del "
"modello stanno nel .prt." % (
sheet["text_count"], len(sheet["texts"])), file=stream)
for text in sheet["texts"][:limit_texts]:
print(" %s" % text, file=stream)
if len(sheet["texts"]) > limit_texts:
print(" ... altri %d (usare -o per averli tutti)" % (
len(sheet["texts"]) - limit_texts), file=stream)
else:
print(" (i disegni non contengono la tabella parametri del modello)",
file=stream)
print(file=stream)
continue
for name, entry in _visible(record, include_system, include_features).items():
status = entry.get("status")
if status == "found":
shown = entry["value"] if entry["value"] != "" else "(vuoto)"
mark = " " if entry.get("confidence", 0) >= 0.95 else "?"
elif status == "encoded_not_decoded":
shown = "<binario non decodificato>"
mark = "~"
elif status == "conflict":
shown = " | ".join(entry.get("candidates", []))
mark = "!"
else:
shown = "(assente)"
mark = "-"
print(f" {mark} {name:<22} {shown}", file=stream)
print(file=stream)
def _preview_path(source: str, dest: str, single: bool) -> str:
"""Decide dove scrivere l'anteprima di `source`.
Senza destinazione la mette accanto all'originale; se la destinazione e'
una cartella ci scrive dentro; un nome esplicito vale solo con un file
solo, altrimenti i file si sovrascriverebbero a vicenda.
"""
name = os.path.basename(source) + ".jpg"
if not dest:
return source + ".jpg"
# La barra finale indica una cartella anche se non esiste ancora; con piu'
# file la destinazione e' comunque una cartella, altrimenti si
# sovrascriverebbero a vicenda.
looks_like_dir = dest.endswith(("/", os.sep)) or os.path.isdir(dest)
if looks_like_dir or not single:
os.makedirs(dest, exist_ok=True)
return os.path.join(dest, name)
return dest
def _save_previews(paths: "list[str]", dest: str, stream) -> None:
single = len(paths) == 1
for source in paths:
shot = read_preview(source)
if shot is None:
print(f"{os.path.basename(source)}: nessuna anteprima nel file",
file=stream)
continue
target = _preview_path(source, dest, single)
shot.save(target)
print(f"Anteprima {shot.width}x{shot.height} salvata in {target}",
file=stream)
def _save_viewers(paths: "list[str]", dest: str, stream) -> None:
from . import viewer
single = len(paths) == 1
for source in paths:
name = os.path.basename(source)
try:
mesh = read_mesh(source)
except (OSError, ValueError) as error:
print(f"{name}: mesh illeggibile ({error})", file=stream)
continue
if mesh is None:
print(f"{name}: nessuna mesh accanto al file. Esportare il "
f"modello in STL da Creo per vederlo in 3D.", file=stream)
continue
stem = split_filename(name)[0]
page = viewer.build(mesh, stem, viewer.describe(mesh))
target = _preview_path(source, dest, single)
target = target[:-4] + ".html" if target.endswith(".jpg") else target
with open(target, "w", encoding="utf-8") as handle:
handle.write(page)
print("Vista 3D di %s (%d triangoli) salvata in %s"
% (stem, len(mesh), target), file=stream)
def _write_csv(records: "list[dict]", path: str, include_system: bool = False,
include_features: bool = False) -> None:
names = sorted({n for r in records for n in _visible(r, include_system, include_features)})
columns = ["file", "model", "kind", "creo_version"] + names
with open(path, "w", newline="", encoding="utf-8-sig") as handle:
writer = csv.DictWriter(handle, fieldnames=columns, delimiter=";")
writer.writeheader()
for record in records:
row = {
"file": record["file"]["name"],
"model": record["model"]["name"],
"kind": record["model"]["kind"],
"creo_version": record["model"]["creo_version"],
}
for name in names:
entry = record["parameters"].get(name, {})
row[name] = entry.get("value") if entry.get("status") == "found" else ""
writer.writerow(row)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="creoparams",
description="Estrae parametri e proprieta' dai file nativi Creo "
"(.prt/.asm/.drw) senza avviare Creo Parametric.",
epilog="Esempi:\n"
" creoparams 9258400201.prt.12 un singolo file\n"
" creoparams 9258400201.prt.12 -o - JSON su stdout\n"
" creoparams /rete/archivio -r --csv i.csv intera cartella",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("inputs", nargs="+", metavar="FILE|CARTELLA")
parser.add_argument("-o", "--json", metavar="FILE",
help="scrive il risultato in JSON; usare '-' per "
"stdout. Con un solo file emette un oggetto, "
"con piu' file una lista")
parser.add_argument("--csv", metavar="FILE",
help="scrive una tabella riepilogativa in CSV")
parser.add_argument("--preview", nargs="?", const="", metavar="FILE|CARTELLA",
help="salva l'anteprima incorporata come JPEG. Senza "
"argomento la scrive accanto al file di origine")
parser.add_argument("--3d", dest="three_d", nargs="?", const="",
metavar="FILE|CARTELLA",
help="genera una pagina HTML per ruotare il modello "
"nel browser, leggendo la mesh (.stl) esportata "
"accanto al file")
parser.add_argument("-r", "--recursive", action="store_true",
help="esplora le sottocartelle")
parser.add_argument("--all-versions", action="store_true",
help="elabora tutte le versioni (.prt.1, .prt.2, ...) "
"invece della sola piu' recente")
parser.add_argument("--system", action="store_true",
help="include anche i parametri generati da Creo "
"(PTC_*, SMT_*)")
parser.add_argument("--features", action="store_true",
help="include anche i parametri appartenenti alle "
"feature invece dei soli parametri di modello")
parser.add_argument("--quiet", action="store_true",
help="non stampa la tabella a video")
return parser
def main(argv: "list[str] | None" = None) -> int:
args = build_parser().parse_args(argv)
explicit, discovered = collect(args.inputs, args.recursive)
if not args.all_versions:
# Un file indicato a mano si elabora com'e': la scelta della versione
# piu' recente riguarda solo l'esplorazione di una cartella.
discovered = latest_versions(discovered)
paths = sorted(set(explicit) | set(discovered))
if not paths:
print("Nessun file Creo trovato.", file=sys.stderr)
return 1
records, failures = [], []
for path in paths:
try:
records.append(extract_file(path, DEFAULT_EXPECTED))
except ugc.NotACreoFile as error:
failures.append((path, str(error)))
except OSError as error:
failures.append((path, str(error)))
if not args.quiet:
_print_table(records, sys.stdout, args.system, args.features)
if args.json:
# Un solo file produce un oggetto, non una lista di un elemento:
# e' la forma piu' comoda da consumare a valle.
payload = records[0] if len(records) == 1 else records
if args.json == "-":
json.dump(payload, sys.stdout, indent=2, ensure_ascii=False)
sys.stdout.write("\n")
else:
with open(args.json, "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2, ensure_ascii=False)
print(f"JSON scritto in {args.json}", file=sys.stderr)
if args.csv:
_write_csv(records, args.csv, args.system, args.features)
print(f"CSV scritto in {args.csv}", file=sys.stderr)
if args.three_d is not None:
_save_viewers([r["source_file"] for r in records], args.three_d, sys.stderr)
if args.preview is not None:
_save_previews(
[r["source_file"] for r in records], args.preview, sys.stderr
)
for path, reason in failures:
print(f"SALTATO {os.path.basename(path)}: {reason}", file=sys.stderr)
# Su un singolo file andato a buon fine il riepilogo e' solo rumore.
if len(paths) > 1 or failures:
print(
f"{len(records)} file elaborati, {len(failures)} saltati.",
file=sys.stderr,
)
return 0 if records else 2