2026-08-06 11:45:39 +02:00
|
|
|
"""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
|
2026-08-06 16:04:46 +02:00
|
|
|
from .extract import (find_mesh, latest_versions, read_mesh,
|
|
|
|
|
read_preview, split_filename)
|
2026-08-06 11:45:39 +02:00
|
|
|
|
|
|
|
|
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,
|
2026-08-06 14:51:39 +02:00
|
|
|
include_features: bool = False, limit_texts: int = 20) -> None:
|
2026-08-06 11:45:39 +02:00
|
|
|
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 '?'}"
|
2026-08-06 14:51:39 +02:00
|
|
|
shot = record.get("preview", {})
|
|
|
|
|
if shot.get("present"):
|
|
|
|
|
header += f" anteprima {shot['width']}x{shot['height']}"
|
2026-08-06 11:45:39 +02:00
|
|
|
print(header, file=stream)
|
|
|
|
|
print("-" * len(header), file=stream)
|
|
|
|
|
|
|
|
|
|
if not record["extraction"]["holds_model_parameters"]:
|
2026-08-06 14:51:39 +02:00
|
|
|
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)
|
2026-08-06 11:45:39 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-06 14:51:39 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-06 16:04:46 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-06 11:45:39 +02:00
|
|
|
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")
|
2026-08-06 14:51:39 +02:00
|
|
|
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")
|
2026-08-06 16:04:46 +02:00
|
|
|
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")
|
2026-08-06 11:45:39 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-08-06 16:04:46 +02:00
|
|
|
if args.three_d is not None:
|
|
|
|
|
_save_viewers([r["source_file"] for r in records], args.three_d, sys.stderr)
|
|
|
|
|
|
2026-08-06 14:51:39 +02:00
|
|
|
if args.preview is not None:
|
|
|
|
|
_save_previews(
|
|
|
|
|
[r["source_file"] for r in records], args.preview, sys.stderr
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-06 11:45:39 +02:00
|
|
|
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
|