Skip to content

Commit 2399559

Browse files
authored
Add files via upload
1 parent e25a40a commit 2399559

3 files changed

Lines changed: 504 additions & 0 deletions

File tree

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
"""ASTERION FCTA-1 native Siemens NX builder.
2+
3+
Run INSIDE Siemens NX through Developer/Tools > Journal > Play.
4+
5+
Outputs native NX .prt files for:
6+
* individual imported facet-body parts;
7+
* the top-level assembly (NX assemblies also use .prt);
8+
* separate master-model drawing parts with A3 sheets and base views.
9+
10+
The source files are STL meshes. The resulting NX parts contain convergent/facet
11+
bodies, not recovered parametric feature history. Rebuild critical parts from the
12+
provided dimensions and tutorials when editable design intent is required.
13+
14+
Tested here only by static validation because Siemens NX is not installed in the
15+
artifact-generation environment. The script uses long-established NXOpen APIs
16+
and writes a detailed build log. API availability and licences can vary by NX
17+
release and installation.
18+
"""
19+
from __future__ import annotations
20+
21+
import csv
22+
import os
23+
import sys
24+
import subprocess
25+
import traceback
26+
from datetime import datetime
27+
from pathlib import Path
28+
29+
30+
def _package_root() -> Path:
31+
return Path(__file__).resolve().parents[1]
32+
33+
34+
PACKAGE_ROOT = Path(os.environ.get("ASTERION_BUILDER_ROOT", str(_package_root()))).resolve()
35+
SOURCE_ROOT = PACKAGE_ROOT
36+
OUTPUT_ROOT = Path(
37+
os.environ.get("ASTERION_NX_OUTPUT", str(PACKAGE_ROOT / "native_output" / "NX_NATIVE"))
38+
).resolve()
39+
OVERWRITE = os.environ.get("ASTERION_OVERWRITE", "0").strip() in {"1", "true", "TRUE", "yes", "YES"}
40+
41+
COMPONENT_MANIFEST = PACKAGE_ROOT / "config" / "nx_component_manifest.csv"
42+
DRAWING_MANIFEST = PACKAGE_ROOT / "config" / "drawing_manifest.csv"
43+
LOG_PATH = OUTPUT_ROOT / "ASTERION_NX_BUILD_LOG.csv"
44+
45+
46+
def _outside_nx_help() -> str:
47+
return (
48+
"NXOpen is provided by Siemens NX and is not available in ordinary Python.\n"
49+
"Do not run this file with python.exe, VS Code Run Python File, or IDLE.\n\n"
50+
"Use one of these supported launch methods:\n"
51+
" 1. Double-click RUN_ASTERION_BUILDER.bat in the package root.\n"
52+
" 2. In Siemens NX: Developer/Tools > Journal > Play, then select this file.\n"
53+
" 3. From an NX Command Prompt: run_journal.exe <this-script>.\n\n"
54+
"Do not install the unrelated PyPI package named nxopen; it is not the Siemens NX API."
55+
)
56+
57+
58+
def _try_relaunch_through_nx() -> bool:
59+
"""Relaunch this journal through Siemens NX when started by normal Python.
60+
61+
Returns True after a launcher was invoked. The launcher performs NX discovery,
62+
sets ASTERION paths, and calls Siemens run_journal.exe.
63+
"""
64+
if os.name != "nt" or os.environ.get("ASTERION_NX_RELAUNCHED") == "1":
65+
return False
66+
67+
launcher = PACKAGE_ROOT / "nxopen" / "run_asterion_builder.ps1"
68+
if not launcher.is_file():
69+
return False
70+
71+
powershell = os.environ.get("SystemRoot", r"C:\Windows") + r"\System32\WindowsPowerShell\v1.0\powershell.exe"
72+
if not Path(powershell).is_file():
73+
powershell = "powershell.exe"
74+
75+
command = [
76+
powershell,
77+
"-NoProfile",
78+
"-ExecutionPolicy",
79+
"Bypass",
80+
"-File",
81+
str(launcher),
82+
]
83+
if OVERWRITE:
84+
command.append("-Overwrite")
85+
86+
print("NXOpen was not found in this Python interpreter.")
87+
print("Attempting to relaunch the builder through Siemens NX...")
88+
completed = subprocess.run(command, cwd=str(PACKAGE_ROOT), check=False)
89+
if completed.returncode != 0:
90+
raise SystemExit(
91+
f"The Siemens NX launcher returned exit code {completed.returncode}.\n\n"
92+
+ _outside_nx_help()
93+
)
94+
return True
95+
96+
97+
class BuildFailure(RuntimeError):
98+
pass
99+
100+
101+
def _read_csv(path: Path) -> list[dict[str, str]]:
102+
with path.open("r", newline="", encoding="utf-8-sig") as handle:
103+
return list(csv.DictReader(handle))
104+
105+
106+
def _safe_remove(path: Path) -> None:
107+
if path.exists():
108+
if not OVERWRITE:
109+
raise BuildFailure(f"Output already exists: {path}. Set ASTERION_OVERWRITE=1 to replace it.")
110+
path.unlink()
111+
112+
113+
def _identity_matrix(NXOpen):
114+
matrix = NXOpen.Matrix3x3()
115+
matrix.Xx, matrix.Xy, matrix.Xz = 1.0, 0.0, 0.0
116+
matrix.Yx, matrix.Yy, matrix.Yz = 0.0, 1.0, 0.0
117+
matrix.Zx, matrix.Zy, matrix.Zz = 0.0, 0.0, 1.0
118+
return matrix
119+
120+
121+
def _write_log(rows: list[dict[str, str]]) -> None:
122+
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
123+
fields = ["timestamp", "stage", "item", "status", "message"]
124+
with LOG_PATH.open("w", newline="", encoding="utf-8") as handle:
125+
writer = csv.DictWriter(handle, fieldnames=fields)
126+
writer.writeheader()
127+
writer.writerows(rows)
128+
129+
130+
def _log(rows, stage, item, status, message=""):
131+
rows.append({
132+
"timestamp": datetime.now().isoformat(timespec="seconds"),
133+
"stage": stage,
134+
"item": item,
135+
"status": status,
136+
"message": str(message).replace("\n", " | ")[:1500],
137+
})
138+
_write_log(rows)
139+
140+
141+
def _import_stl_into_part(NXOpen, part, source_stl: Path):
142+
"""Import an STL using the standard NXOpen STLImporter."""
143+
importer = part.ImportManager.CreateStlImporter()
144+
try:
145+
importer.FileName = str(source_stl)
146+
importer.FileUnits = NXOpen.STLImporter.FileUnitsType.Millimeters
147+
importer.AngularTolerance = NXOpen.STLImporter.AngularToleranceType.Fine
148+
importer.HideSmoothEdges = True
149+
importer.DisplayInformation = False
150+
importer.Commit()
151+
finally:
152+
importer.Destroy()
153+
154+
155+
def _new_metric_part(NXOpen, session, output_path: Path):
156+
_safe_remove(output_path)
157+
output_path.parent.mkdir(parents=True, exist_ok=True)
158+
return session.Parts.NewDisplay(str(output_path), NXOpen.Part.Units.Millimeters)
159+
160+
161+
def _save_part(part, output_path: Path):
162+
status = part.SaveAs(str(output_path))
163+
try:
164+
status.Dispose()
165+
except Exception:
166+
pass
167+
168+
169+
def build_native_parts(NXOpen, session, rows, manifest):
170+
created: dict[str, Path] = {}
171+
for item in manifest:
172+
part_name = item["nx_part_name"].strip()
173+
source_path = SOURCE_ROOT / item["source_stl"].strip()
174+
output_path = OUTPUT_ROOT / "parts" / part_name
175+
try:
176+
if not source_path.is_file():
177+
raise BuildFailure(f"Missing source STL: {source_path}")
178+
part = _new_metric_part(NXOpen, session, output_path)
179+
_import_stl_into_part(NXOpen, part, source_path)
180+
try:
181+
part.SetUserAttribute("ASTERION_ID", -1, item["id"], NXOpen.Update.Option.Now)
182+
part.SetUserAttribute("ASTERION_SOURCE_STL", -1, item["source_stl"], NXOpen.Update.Option.Now)
183+
part.SetUserAttribute("ASTERION_DESCRIPTION", -1, item["description"], NXOpen.Update.Option.Now)
184+
except Exception:
185+
# Attributes are useful but non-critical across NX releases.
186+
pass
187+
_save_part(part, output_path)
188+
created[part_name] = output_path
189+
_log(rows, "PART", part_name, "PASS", f"Imported {source_path.name}")
190+
except Exception as exc:
191+
_log(rows, "PART", part_name, "FAIL", f"{type(exc).__name__}: {exc}")
192+
raise
193+
return created
194+
195+
196+
def build_top_assembly(NXOpen, session, rows, manifest, created_parts):
197+
assembly_name = "AST-0000-ASTERION-FCTA-1-ASSY.prt"
198+
output_path = OUTPUT_ROOT / "assemblies" / assembly_name
199+
assembly = _new_metric_part(NXOpen, session, output_path)
200+
component_assembly = assembly.ComponentAssembly
201+
origin = NXOpen.Point3d(0.0, 0.0, 0.0)
202+
orientation = _identity_matrix(NXOpen)
203+
204+
included = [m for m in manifest if m["include_in_top_assembly"].strip() == "1"]
205+
for item in included:
206+
part_path = created_parts[item["nx_part_name"].strip()]
207+
component_name = item["component_name"].strip()
208+
try:
209+
result = component_assembly.AddComponent(
210+
str(part_path), "Entire Part", component_name, origin, orientation, -1
211+
)
212+
# Python wrappers commonly return (component, PartLoadStatus).
213+
if isinstance(result, tuple) and len(result) > 1:
214+
load_status = result[1]
215+
try:
216+
load_status.Dispose()
217+
except Exception:
218+
pass
219+
_log(rows, "ASSEMBLY_COMPONENT", component_name, "PASS", part_path.name)
220+
except Exception as exc:
221+
_log(rows, "ASSEMBLY_COMPONENT", component_name, "FAIL", f"{type(exc).__name__}: {exc}")
222+
raise
223+
224+
try:
225+
assembly.SetUserAttribute("ASTERION_CONFIGURATION", -1, "FCTA-1 V1.0", NXOpen.Update.Option.Now)
226+
except Exception:
227+
pass
228+
_save_part(assembly, output_path)
229+
_log(rows, "ASSEMBLY", assembly_name, "PASS", f"{len(included)} components at absolute origin")
230+
return output_path
231+
232+
233+
def _find_model_view(part, wanted: str):
234+
wanted_upper = wanted.upper()
235+
candidates = []
236+
try:
237+
candidates = list(part.ModelingViews.ToArray())
238+
except Exception:
239+
candidates = list(part.ModelingViews)
240+
for view in candidates:
241+
name = getattr(view, "Name", "")
242+
if str(name).upper() == wanted_upper:
243+
return view
244+
# Fallback to FindObject for installations using standard names.
245+
for name in (wanted, wanted.upper(), wanted.capitalize()):
246+
try:
247+
return part.ModelingViews.FindObject(name)
248+
except Exception:
249+
pass
250+
raise BuildFailure(f"Could not find standard modeling view: {wanted}")
251+
252+
253+
def _insert_a3_sheet(NXOpen, drawing_part, sheet_name: str, scale_denominator: float):
254+
return drawing_part.DrawingSheets.InsertSheet(
255+
sheet_name,
256+
NXOpen.Drawings.DrawingSheet.StandardSheetSize.A3,
257+
1.0,
258+
float(scale_denominator),
259+
NXOpen.Drawings.DrawingSheet.ProjectionAngleType.ThirdAngle,
260+
)
261+
262+
263+
def _add_standard_views(NXOpen, drawing_part, sheet, denominator: float):
264+
scale = 1.0 / float(denominator)
265+
views = sheet.SheetDraftingViews
266+
front = _find_model_view(drawing_part, "Front")
267+
top = _find_model_view(drawing_part, "Top")
268+
right = _find_model_view(drawing_part, "Right")
269+
iso = None
270+
for iso_name in ("Trimetric", "Isometric", "TFR-ISO"):
271+
try:
272+
iso = _find_model_view(drawing_part, iso_name)
273+
break
274+
except Exception:
275+
continue
276+
277+
views.CreateBaseView(front, NXOpen.Point3d(115.0, 105.0, 0.0), scale, False)
278+
views.CreateBaseView(top, NXOpen.Point3d(115.0, 205.0, 0.0), scale, False)
279+
views.CreateBaseView(right, NXOpen.Point3d(265.0, 105.0, 0.0), scale, False)
280+
if iso is not None:
281+
views.CreateBaseView(iso, NXOpen.Point3d(270.0, 205.0, 0.0), scale * 0.75, False)
282+
283+
284+
def build_drawing_parts(NXOpen, session, rows, drawing_manifest, model_paths):
285+
drawing_dir = OUTPUT_ROOT / "drawings"
286+
drawing_dir.mkdir(parents=True, exist_ok=True)
287+
for item in drawing_manifest:
288+
model_name = item["model_prt"].strip()
289+
drawing_name = item["drawing_prt"].strip()
290+
output_path = drawing_dir / drawing_name
291+
model_path = model_paths.get(model_name)
292+
if model_path is None:
293+
_log(rows, "DRAWING", drawing_name, "FAIL", f"Model not built: {model_name}")
294+
continue
295+
try:
296+
drawing_part = _new_metric_part(NXOpen, session, output_path)
297+
origin = NXOpen.Point3d(0.0, 0.0, 0.0)
298+
orientation = _identity_matrix(NXOpen)
299+
result = drawing_part.ComponentAssembly.AddComponent(
300+
str(model_path), "Entire Part", "MASTER_MODEL", origin, orientation, -1
301+
)
302+
if isinstance(result, tuple) and len(result) > 1:
303+
try:
304+
result[1].Dispose()
305+
except Exception:
306+
pass
307+
308+
sheet = _insert_a3_sheet(
309+
NXOpen, drawing_part, item["sheet_name"].strip(), float(item["scale_denominator"])
310+
)
311+
_add_standard_views(NXOpen, drawing_part, sheet, float(item["scale_denominator"]))
312+
try:
313+
drawing_part.SetUserAttribute("ASTERION_DRAWING_TITLE", -1, item["title"], NXOpen.Update.Option.Now)
314+
drawing_part.SetUserAttribute("ASTERION_MASTER_MODEL", -1, model_name, NXOpen.Update.Option.Now)
315+
except Exception:
316+
pass
317+
_save_part(drawing_part, output_path)
318+
_log(rows, "DRAWING", drawing_name, "PASS", f"A3 sheet linked to {model_name}")
319+
except Exception as exc:
320+
# Save a linked drawing/master-model part where possible, then continue.
321+
try:
322+
_save_part(drawing_part, output_path)
323+
except Exception:
324+
pass
325+
_log(
326+
rows,
327+
"DRAWING",
328+
drawing_name,
329+
"PARTIAL",
330+
f"Linked model saved, but sheet/view creation needs manual completion: {type(exc).__name__}: {exc}",
331+
)
332+
333+
334+
def main():
335+
try:
336+
import NXOpen
337+
import NXOpen.Drawings
338+
except ImportError:
339+
if _try_relaunch_through_nx():
340+
return
341+
raise SystemExit(_outside_nx_help())
342+
343+
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
344+
rows: list[dict[str, str]] = []
345+
_log(rows, "BUILD", "ASTERION", "START", f"Output={OUTPUT_ROOT}; overwrite={OVERWRITE}")
346+
347+
component_manifest = _read_csv(COMPONENT_MANIFEST)
348+
drawing_manifest = _read_csv(DRAWING_MANIFEST)
349+
session = NXOpen.Session.GetSession()
350+
listing = session.ListingWindow
351+
listing.Open()
352+
listing.WriteFullline("ASTERION native NX build started")
353+
listing.WriteFullline(f"Output folder: {OUTPUT_ROOT}")
354+
355+
try:
356+
created_parts = build_native_parts(NXOpen, session, rows, component_manifest)
357+
assembly_path = build_top_assembly(NXOpen, session, rows, component_manifest, created_parts)
358+
model_paths = dict(created_parts)
359+
model_paths[assembly_path.name] = assembly_path
360+
build_drawing_parts(NXOpen, session, rows, drawing_manifest, model_paths)
361+
_log(rows, "BUILD", "ASTERION", "COMPLETE", "Review build log and native files.")
362+
listing.WriteFullline("ASTERION native NX build completed. Review ASTERION_NX_BUILD_LOG.csv")
363+
except Exception as exc:
364+
_log(rows, "BUILD", "ASTERION", "FAIL", traceback.format_exc())
365+
listing.WriteFullline(f"ASTERION build failed: {type(exc).__name__}: {exc}")
366+
raise
367+
finally:
368+
listing.Close()
369+
370+
371+
if __name__ == "__main__":
372+
main()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
param(
2+
[Parameter(Mandatory=$true)]
3+
[string]$NxExecutable,
4+
[string]$OutputFolder = "$PSScriptRoot\..\native_output\NX_NATIVE",
5+
[switch]$Overwrite
6+
)
7+
8+
$nxBin = Split-Path ([System.IO.Path]::GetFullPath($NxExecutable)) -Parent
9+
$params = @{
10+
NxBin = $nxBin
11+
OutputFolder = $OutputFolder
12+
Gui = $true
13+
}
14+
if ($Overwrite) { $params.Overwrite = $true }
15+
& (Join-Path $PSScriptRoot "run_asterion_builder.ps1") @params
16+
exit $LASTEXITCODE

0 commit comments

Comments
 (0)