Refined gitignore

This commit is contained in:
Christian Anetzberger
2026-01-22 20:28:35 +01:00
parent 7d22cfc9b9
commit e18c14750e
94 changed files with 59 additions and 47760 deletions

61
.gitignore vendored
View File

@@ -1,2 +1,59 @@
_jobs
.venv
# =========================
# Python
# =========================
__pycache__/
*.py[cod]
*$py.class
# Virtual environments
.venv/
venv/
env/
# =========================
# macOS
# =========================
.DS_Store
.AppleDouble
.LSOverride
# =========================
# IDEs / Editors
# =========================
.vscode/
.idea/
*.swp
*.swo
# =========================
# Logs
# =========================
*.log
# =========================
# Job runtime data (generated)
# =========================
_jobs/
_jobs/**
# =========================
# Generated outputs
# =========================
*.dxf
*.svg
*.json
*.FCStd
# If you want to keep example outputs, comment out the lines above selectively
# =========================
# Temporary files
# =========================
*.tmp
*.bak
*.old
# =========================
# OS / misc
# =========================
Thumbs.db

View File

@@ -1,6 +0,0 @@
{
"ok": false,
"error_type": "SystemExit",
"error": "STEP File not found not found in working directory",
"traceback": "Traceback (most recent call last):\n File \"/Users/christiananetzberger/development/stepanalyser_web/_jobs/0e1d23a0181f/run_stepanalyser.py\", line 28, in <module>\n exec(compile(code, \"stepanalyser.py\", \"exec\"), {\"__name__\": \"__main__\"})\n File \"stepanalyser.py\", line 417, in <module>\n main()\n File \"stepanalyser.py\", line 285, in main\n raise SystemExit(\"STEP File not found not found in working directory\")\nSystemExit: STEP File not found not found in working directory\n"
}

View File

@@ -1,3 +0,0 @@
=== STEPANALYSER START ===
Material: stainless Density: 8000.0
RUNNER ERROR: SystemExit STEP File not found not found in working directory

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(ok, payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
# Ensure FreeCAD can find user-installed Mods (SheetMetal etc.)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
# Also add SheetMetal folder explicitly (some setups need it)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
# argv injection
sys.argv = ["stepanalyser.py", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(False, payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,417 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: ./test.step
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import sys
import json
import argparse
import traceback
from datetime import datetime
# -----------------------------
# Configuration
# -----------------------------
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
# -----------------------------
# Helpers
# -----------------------------
def mm2_to_m2(x):
return x / 1_000_000.0
def mm_to_m(x):
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation for sheet-metal solids:
- Collect planar faces
- Cluster by (normalized) face normal direction
- For each cluster, compute plane offsets d = n·p for face points
- Thickness candidates are small positive differences between distinct d values
- Fallback: use distToShape on representative face pairs
Returns thickness in mm or None.
"""
import math
import Part
shape = part_obj.Shape
# --- helpers ---
def vec_to_tuple(v):
return (float(v.x), float(v.y), float(v.z))
def norm(v):
l = math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
"""
Make the normal direction canonical so +n and -n map consistently:
Flip so the first significant component is positive.
"""
# convert to tuple for easier checks
x, y, z = float(n.x), float(n.y), float(n.z)
# find first component with magnitude > eps
eps = 1e-9
if abs(x) > eps:
if x < 0: return n.multiply(-1)
elif abs(y) > eps:
if y < 0: return n.multiply(-1)
elif abs(z) > eps:
if z < 0: return n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
# cos(angle) = n1·n2
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
return face.valueAt((u0+u1)*0.5, (v0+v1)*0.5), face.normalAt((u0+u1)*0.5, (v0+v1)*0.5)
# --- collect planar faces ---
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0: # mm², ignore tiny faces
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p)) # plane offset for n·x = d
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
# --- cluster by normal direction ---
# Tolerance: within ~2 degrees
cos_tol = math.cos(math.radians(2.0))
clusters = [] # each: {"n": normal, "faces": [(area, face, d), ...]}
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
# --- build thickness candidates from d-values inside each cluster ---
# For a sheet, same-normal planes occur at (outer) and (inner) offsets,
# so distinct d-values differ ~thickness.
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0: # mm range guard (tune if needed)
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
# unique d-values with binning (0.01 mm)
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
# candidate: smallest positive gap between uniq planes
# Often thickness is the smallest meaningful separation.
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i-1])
add_candidate(gap)
# --- if candidates exist, pick most frequent bin (mode-ish) ---
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket)//2]
if candidates:
return pick_mode(candidates, 0.01)
# --- Fallback: distToShape between face pairs in same normal cluster ---
# Slower but can rescue cases where d-values are too noisy.
# We try only top clusters and top faces.
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
# compare each face to others in same cluster; minimal non-zero distance tends to thickness
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i+1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0] # returns (dist, pts, info)
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
# -----------------------------
# Main
# -----------------------------
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = os.path.join(cwd, "03341791-01_01.step")
out_dxf = os.path.join(cwd, "03341791-01_01.dxf")
out_json = os.path.join(cwd, "03341791-01_01-result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Material:", material_key, "Density:", density, flush=True)
if not os.path.exists(step_path):
raise SystemExit("STEP File not found not found in working directory")
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed")
importDXF.export(sketches, out_dxf)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR
},
"flat": {
"bbox_mm": {"width": bbox_w, "height": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_net_mm2": area_net_mm2
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg
},
"warnings": warnings
}
with open(out_json, "w") as f:
json.dump(result, f, indent=2)
doc.saveAs(out_fcstd)
print("OK: flat.dxf + result.json written", flush=True)
except Exception as e:
try:
doc.saveAs(out_fcstd)
except Exception:
pass
err = {
"ok": False,
"error": str(e),
"traceback": traceback.format_exc()
}
with open(out_json, "w") as f:
json.dump(err, f, indent=2)
print("ERROR:", e, flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:21:26",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01.step",
"step_filename": "03341701-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 1.5,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 195.5685834705776,
"height_mm": 1.5000000000000036
},
"area_bbox_mm2": 293.3528752058671,
"area_bbox_m2": 0.0002933528752058671,
"area_net_mm2": 46219.020878948264,
"area_net_m2": 0.04621902087894826
},
"weight": {
"bbox_kg": 0.0035202345024704053,
"net_kg": 0.5546282505473791,
"bbox_g": 3.520234502470405,
"net_g": 554.628250547379
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/debug_last.FCStd"
},
"warnings": []
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:21:26",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01.step",
"step_filename": "03341701-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 1.5,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 195.5685834705776,
"height_mm": 1.5000000000000036
},
"area_bbox_mm2": 293.3528752058671,
"area_bbox_m2": 0.0002933528752058671,
"area_net_mm2": 46219.020878948264,
"area_net_m2": 0.04621902087894826
},
"weight": {
"bbox_kg": 0.0035202345024704053,
"net_kg": 0.5546282505473791,
"bbox_g": 3.520234502470405,
"net_g": 554.628250547379
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/03341701-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/4699191e2667/debug_last.FCStd"
},
"warnings": []
}

View File

@@ -1,7 +0,0 @@
=== STEPANALYSER START ===
Input: 03341701-01_01.step
Material: stainless Density: 8000.0
Error: Failed to open library "/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib"! Error: dlopen(/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib, 0x0005): tried: '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file, not in dyld cache)!
Auto-detecting thickness...
Detected thickness: 1.500 mm
OK: wrote DXF + JSON

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
if not os.path.exists("03341701-01_01.step"):
raise SystemExit("Uploaded STEP file missing: " + "03341701-01_01.step")
# Ensure FreeCAD can find user Mods (SheetMetal)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
sys.argv = ["stepanalyser.py", "--input", "03341701-01_01.step", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,470 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: --input <file.step|file.stp> (relative to cwd or absolute)
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- <basename>_flat.dxf
- <basename>_result.json
- <basename>_debug.FCStd
Additionally (compat):
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import json
import argparse
import traceback
from datetime import datetime
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
def mm2_to_m2(x: float) -> float:
return x / 1_000_000.0
def mm_to_m(x: float) -> float:
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation:
- cluster planar faces by normal direction
- estimate thickness from plane offsets
- fallback to distToShape on face pairs
"""
import math
shape = part_obj.Shape
def norm(v):
l = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
eps = 1e-9
x, y, z = float(n.x), float(n.y), float(n.z)
if abs(x) > eps:
return n if x > 0 else n.multiply(-1)
if abs(y) > eps:
return n if y > 0 else n.multiply(-1)
if abs(z) > eps:
return n if z > 0 else n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
u = (u0 + u1) * 0.5
v = (v0 + v1) * 0.5
return face.valueAt(u, v), face.normalAt(u, v)
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0:
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p))
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
cos_tol = math.cos(math.radians(2.0))
clusters = []
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0:
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i - 1])
add_candidate(gap)
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket) // 2]
if candidates:
return pick_mode(candidates, 0.01)
# fallback: distToShape (slower)
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i + 1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0]
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
def write_json(path, payload):
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
def try_copy(src, dst):
try:
if src != dst and os.path.exists(src):
# overwrite
with open(src, "rb") as fsrc:
data = fsrc.read()
with open(dst, "wb") as fdst:
fdst.write(data)
except Exception:
pass
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="STEP file path (.step/.stp), absolute or relative to cwd")
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = args.input
if not os.path.isabs(step_path):
step_path = os.path.join(cwd, step_path)
if not os.path.exists(step_path):
raise SystemExit(f"STEP file not found in working directory: {step_path}")
base = os.path.splitext(os.path.basename(step_path))[0]
out_dxf_named = os.path.join(cwd, f"{base}_flat.dxf")
out_json_named = os.path.join(cwd, f"{base}_result.json")
out_fcstd_named = os.path.join(cwd, f"{base}_debug.FCStd")
# compat outputs for the web UI
out_dxf = os.path.join(cwd, "flat.dxf")
out_json = os.path.join(cwd, "result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Input:", os.path.basename(step_path), flush=True)
print("Material:", material_key, "Density:", density, flush=True)
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception as e:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception as e:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available (V1/V2). Check SheetMetal installation.")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
if thickness_mm <= 0:
raise RuntimeError("Invalid thickness (<= 0)")
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed; trying V1 fallback.")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed (no unfolded shape or sketches).")
# Export DXF (named)
importDXF.export(sketches, out_dxf_named)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
"flat": {
"bbox_mm": {"width_mm": bbox_w, "height_mm": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_bbox_m2": area_bbox_m2,
"area_net_mm2": area_net_mm2,
"area_net_m2": area_net_m2,
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg,
"bbox_g": mass_bbox_kg * 1000.0,
"net_g": mass_net_kg * 1000.0,
},
"output": {
"dxf_named": os.path.abspath(out_dxf_named),
"json_named": os.path.abspath(out_json_named),
"fcstd_named": os.path.abspath(out_fcstd_named),
"dxf": os.path.abspath(out_dxf),
"json": os.path.abspath(out_json),
"fcstd": os.path.abspath(out_fcstd),
},
"warnings": warnings,
}
write_json(out_json_named, result)
# Save debug doc (named)
doc.saveAs(out_fcstd_named)
# Compat copies for web UI
try_copy(out_dxf_named, out_dxf)
try_copy(out_json_named, out_json)
try_copy(out_fcstd_named, out_fcstd)
print("OK: wrote DXF + JSON", flush=True)
except Exception as e:
# Always write named + compat error JSON
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": args.thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
}
try:
write_json(out_json_named, err)
except Exception:
pass
try:
write_json(out_json, err)
except Exception:
pass
try:
doc.saveAs(out_fcstd_named)
try_copy(out_fcstd_named, out_fcstd)
except Exception:
pass
print("ERROR:", str(e), flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
# Catch also SystemExit/argparse exits so we still emit result.json
try:
main()
except BaseException as e:
cwd = os.getcwd()
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
}
try:
with open(os.path.join(cwd, "result.json"), "w", encoding="utf-8") as f:
json.dump(err, f, indent=2, ensure_ascii=False)
except Exception:
pass
print("FATAL:", err["error_type"], err["error"], flush=True)
os._exit(1)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:01:43",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01.step",
"step_filename": "03341701-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 1.5,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 195.5685834705776,
"height_mm": 1.5000000000000036
},
"area_bbox_mm2": 293.3528752058671,
"area_bbox_m2": 0.0002933528752058671,
"area_net_mm2": 46219.020878948264,
"area_net_m2": 0.04621902087894826
},
"weight": {
"bbox_kg": 0.0035202345024704053,
"net_kg": 0.5546282505473791,
"bbox_g": 3.520234502470405,
"net_g": 554.628250547379
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/debug_last.FCStd"
},
"warnings": []
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:01:43",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01.step",
"step_filename": "03341701-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 1.5,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 195.5685834705776,
"height_mm": 1.5000000000000036
},
"area_bbox_mm2": 293.3528752058671,
"area_bbox_m2": 0.0002933528752058671,
"area_net_mm2": 46219.020878948264,
"area_net_m2": 0.04621902087894826
},
"weight": {
"bbox_kg": 0.0035202345024704053,
"net_kg": 0.5546282505473791,
"bbox_g": 3.520234502470405,
"net_g": 554.628250547379
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/03341701-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/5d7ecc58fcc3/debug_last.FCStd"
},
"warnings": []
}

View File

@@ -1,7 +0,0 @@
=== STEPANALYSER START ===
Input: 03341701-01_01.step
Material: stainless Density: 8000.0
Error: Failed to open library "/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib"! Error: dlopen(/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib, 0x0005): tried: '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file, not in dyld cache)!
Auto-detecting thickness...
Detected thickness: 1.500 mm
OK: wrote DXF + JSON

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
if not os.path.exists("03341701-01_01.step"):
raise SystemExit("Uploaded STEP file missing: " + "03341701-01_01.step")
# Ensure FreeCAD can find user Mods (SheetMetal installed in user profile)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
sys.argv = ["stepanalyser.py", "--input", "03341701-01_01.step", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,470 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: --input <file.step|file.stp> (relative to cwd or absolute)
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- <basename>_flat.dxf
- <basename>_result.json
- <basename>_debug.FCStd
Additionally (compat):
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import json
import argparse
import traceback
from datetime import datetime
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
def mm2_to_m2(x: float) -> float:
return x / 1_000_000.0
def mm_to_m(x: float) -> float:
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation:
- cluster planar faces by normal direction
- estimate thickness from plane offsets
- fallback to distToShape on face pairs
"""
import math
shape = part_obj.Shape
def norm(v):
l = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
eps = 1e-9
x, y, z = float(n.x), float(n.y), float(n.z)
if abs(x) > eps:
return n if x > 0 else n.multiply(-1)
if abs(y) > eps:
return n if y > 0 else n.multiply(-1)
if abs(z) > eps:
return n if z > 0 else n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
u = (u0 + u1) * 0.5
v = (v0 + v1) * 0.5
return face.valueAt(u, v), face.normalAt(u, v)
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0:
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p))
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
cos_tol = math.cos(math.radians(2.0))
clusters = []
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0:
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i - 1])
add_candidate(gap)
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket) // 2]
if candidates:
return pick_mode(candidates, 0.01)
# fallback: distToShape (slower)
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i + 1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0]
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
def write_json(path, payload):
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
def try_copy(src, dst):
try:
if src != dst and os.path.exists(src):
# overwrite
with open(src, "rb") as fsrc:
data = fsrc.read()
with open(dst, "wb") as fdst:
fdst.write(data)
except Exception:
pass
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="STEP file path (.step/.stp), absolute or relative to cwd")
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = args.input
if not os.path.isabs(step_path):
step_path = os.path.join(cwd, step_path)
if not os.path.exists(step_path):
raise SystemExit(f"STEP file not found in working directory: {step_path}")
base = os.path.splitext(os.path.basename(step_path))[0]
out_dxf_named = os.path.join(cwd, f"{base}_flat.dxf")
out_json_named = os.path.join(cwd, f"{base}_result.json")
out_fcstd_named = os.path.join(cwd, f"{base}_debug.FCStd")
# compat outputs for the web UI
out_dxf = os.path.join(cwd, "flat.dxf")
out_json = os.path.join(cwd, "result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Input:", os.path.basename(step_path), flush=True)
print("Material:", material_key, "Density:", density, flush=True)
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception as e:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception as e:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available (V1/V2). Check SheetMetal installation.")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
if thickness_mm <= 0:
raise RuntimeError("Invalid thickness (<= 0)")
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed; trying V1 fallback.")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed (no unfolded shape or sketches).")
# Export DXF (named)
importDXF.export(sketches, out_dxf_named)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
"flat": {
"bbox_mm": {"width_mm": bbox_w, "height_mm": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_bbox_m2": area_bbox_m2,
"area_net_mm2": area_net_mm2,
"area_net_m2": area_net_m2,
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg,
"bbox_g": mass_bbox_kg * 1000.0,
"net_g": mass_net_kg * 1000.0,
},
"output": {
"dxf_named": os.path.abspath(out_dxf_named),
"json_named": os.path.abspath(out_json_named),
"fcstd_named": os.path.abspath(out_fcstd_named),
"dxf": os.path.abspath(out_dxf),
"json": os.path.abspath(out_json),
"fcstd": os.path.abspath(out_fcstd),
},
"warnings": warnings,
}
write_json(out_json_named, result)
# Save debug doc (named)
doc.saveAs(out_fcstd_named)
# Compat copies for web UI
try_copy(out_dxf_named, out_dxf)
try_copy(out_json_named, out_json)
try_copy(out_fcstd_named, out_fcstd)
print("OK: wrote DXF + JSON", flush=True)
except Exception as e:
# Always write named + compat error JSON
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": args.thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
}
try:
write_json(out_json_named, err)
except Exception:
pass
try:
write_json(out_json, err)
except Exception:
pass
try:
doc.saveAs(out_fcstd_named)
try_copy(out_fcstd_named, out_fcstd)
except Exception:
pass
print("ERROR:", str(e), flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
# Catch also SystemExit/argparse exits so we still emit result.json
try:
main()
except BaseException as e:
cwd = os.getcwd()
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
}
try:
with open(os.path.join(cwd, "result.json"), "w", encoding="utf-8") as f:
json.dump(err, f, indent=2, ensure_ascii=False)
except Exception:
pass
print("FATAL:", err["error_type"], err["error"], flush=True)
os._exit(1)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:21:36",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test.step",
"step_filename": "test.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 2.3000000000000007,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 57.8000000000001,
"height_mm": 120.47698930976972
},
"area_bbox_mm2": 6963.569982104702,
"area_bbox_m2": 0.0069635699821047016,
"area_net_mm2": 14502.94311573923,
"area_net_m2": 0.01450294311573923
},
"weight": {
"bbox_kg": 0.12812968767072655,
"net_kg": 0.2668541533296019,
"bbox_g": 128.12968767072655,
"net_g": 266.8541533296019
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/debug_last.FCStd"
},
"warnings": []
}

View File

@@ -1,7 +0,0 @@
=== STEPANALYSER START ===
Input: test.step
Material: stainless Density: 8000.0
Error: Failed to open library "/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib"! Error: dlopen(/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib, 0x0005): tried: '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file, not in dyld cache)!
Auto-detecting thickness...
Detected thickness: 2.300 mm
OK: wrote DXF + JSON

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
if not os.path.exists("test.step"):
raise SystemExit("Uploaded STEP file missing: " + "test.step")
# Ensure FreeCAD can find user Mods (SheetMetal)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
sys.argv = ["stepanalyser.py", "--input", "test.step", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,470 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: --input <file.step|file.stp> (relative to cwd or absolute)
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- <basename>_flat.dxf
- <basename>_result.json
- <basename>_debug.FCStd
Additionally (compat):
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import json
import argparse
import traceback
from datetime import datetime
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
def mm2_to_m2(x: float) -> float:
return x / 1_000_000.0
def mm_to_m(x: float) -> float:
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation:
- cluster planar faces by normal direction
- estimate thickness from plane offsets
- fallback to distToShape on face pairs
"""
import math
shape = part_obj.Shape
def norm(v):
l = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
eps = 1e-9
x, y, z = float(n.x), float(n.y), float(n.z)
if abs(x) > eps:
return n if x > 0 else n.multiply(-1)
if abs(y) > eps:
return n if y > 0 else n.multiply(-1)
if abs(z) > eps:
return n if z > 0 else n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
u = (u0 + u1) * 0.5
v = (v0 + v1) * 0.5
return face.valueAt(u, v), face.normalAt(u, v)
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0:
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p))
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
cos_tol = math.cos(math.radians(2.0))
clusters = []
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0:
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i - 1])
add_candidate(gap)
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket) // 2]
if candidates:
return pick_mode(candidates, 0.01)
# fallback: distToShape (slower)
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i + 1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0]
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
def write_json(path, payload):
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
def try_copy(src, dst):
try:
if src != dst and os.path.exists(src):
# overwrite
with open(src, "rb") as fsrc:
data = fsrc.read()
with open(dst, "wb") as fdst:
fdst.write(data)
except Exception:
pass
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="STEP file path (.step/.stp), absolute or relative to cwd")
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = args.input
if not os.path.isabs(step_path):
step_path = os.path.join(cwd, step_path)
if not os.path.exists(step_path):
raise SystemExit(f"STEP file not found in working directory: {step_path}")
base = os.path.splitext(os.path.basename(step_path))[0]
out_dxf_named = os.path.join(cwd, f"{base}_flat.dxf")
out_json_named = os.path.join(cwd, f"{base}_result.json")
out_fcstd_named = os.path.join(cwd, f"{base}_debug.FCStd")
# compat outputs for the web UI
out_dxf = os.path.join(cwd, "flat.dxf")
out_json = os.path.join(cwd, "result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Input:", os.path.basename(step_path), flush=True)
print("Material:", material_key, "Density:", density, flush=True)
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception as e:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception as e:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available (V1/V2). Check SheetMetal installation.")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
if thickness_mm <= 0:
raise RuntimeError("Invalid thickness (<= 0)")
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed; trying V1 fallback.")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed (no unfolded shape or sketches).")
# Export DXF (named)
importDXF.export(sketches, out_dxf_named)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
"flat": {
"bbox_mm": {"width_mm": bbox_w, "height_mm": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_bbox_m2": area_bbox_m2,
"area_net_mm2": area_net_mm2,
"area_net_m2": area_net_m2,
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg,
"bbox_g": mass_bbox_kg * 1000.0,
"net_g": mass_net_kg * 1000.0,
},
"output": {
"dxf_named": os.path.abspath(out_dxf_named),
"json_named": os.path.abspath(out_json_named),
"fcstd_named": os.path.abspath(out_fcstd_named),
"dxf": os.path.abspath(out_dxf),
"json": os.path.abspath(out_json),
"fcstd": os.path.abspath(out_fcstd),
},
"warnings": warnings,
}
write_json(out_json_named, result)
# Save debug doc (named)
doc.saveAs(out_fcstd_named)
# Compat copies for web UI
try_copy(out_dxf_named, out_dxf)
try_copy(out_json_named, out_json)
try_copy(out_fcstd_named, out_fcstd)
print("OK: wrote DXF + JSON", flush=True)
except Exception as e:
# Always write named + compat error JSON
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": args.thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
}
try:
write_json(out_json_named, err)
except Exception:
pass
try:
write_json(out_json, err)
except Exception:
pass
try:
doc.saveAs(out_fcstd_named)
try_copy(out_fcstd_named, out_fcstd)
except Exception:
pass
print("ERROR:", str(e), flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
# Catch also SystemExit/argparse exits so we still emit result.json
try:
main()
except BaseException as e:
cwd = os.getcwd()
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
}
try:
with open(os.path.join(cwd, "result.json"), "w", encoding="utf-8") as f:
json.dump(err, f, indent=2, ensure_ascii=False)
except Exception:
pass
print("FATAL:", err["error_type"], err["error"], flush=True)
os._exit(1)

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:21:36",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test.step",
"step_filename": "test.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 2.3000000000000007,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 57.8000000000001,
"height_mm": 120.47698930976972
},
"area_bbox_mm2": 6963.569982104702,
"area_bbox_m2": 0.0069635699821047016,
"area_net_mm2": 14502.94311573923,
"area_net_m2": 0.01450294311573923
},
"weight": {
"bbox_kg": 0.12812968767072655,
"net_kg": 0.2668541533296019,
"bbox_g": 128.12968767072655,
"net_g": 266.8541533296019
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/test_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/79b91fe12e25/debug_last.FCStd"
},
"warnings": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:18:20",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01.step",
"step_filename": "03341701-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 1.5,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 195.5685834705776,
"height_mm": 1.5000000000000036
},
"area_bbox_mm2": 293.3528752058671,
"area_bbox_m2": 0.0002933528752058671,
"area_net_mm2": 46219.020878948264,
"area_net_m2": 0.04621902087894826
},
"weight": {
"bbox_kg": 0.0035202345024704053,
"net_kg": 0.5546282505473791,
"bbox_g": 3.520234502470405,
"net_g": 554.628250547379
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/debug_last.FCStd"
},
"warnings": []
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:18:20",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01.step",
"step_filename": "03341701-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 1.5,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 195.5685834705776,
"height_mm": 1.5000000000000036
},
"area_bbox_mm2": 293.3528752058671,
"area_bbox_m2": 0.0002933528752058671,
"area_net_mm2": 46219.020878948264,
"area_net_m2": 0.04621902087894826
},
"weight": {
"bbox_kg": 0.0035202345024704053,
"net_kg": 0.5546282505473791,
"bbox_g": 3.520234502470405,
"net_g": 554.628250547379
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/03341701-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/8693d05646e0/debug_last.FCStd"
},
"warnings": []
}

View File

@@ -1,7 +0,0 @@
=== STEPANALYSER START ===
Input: 03341701-01_01.step
Material: stainless Density: 8000.0
Error: Failed to open library "/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib"! Error: dlopen(/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib, 0x0005): tried: '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file, not in dyld cache)!
Auto-detecting thickness...
Detected thickness: 1.500 mm
OK: wrote DXF + JSON

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
if not os.path.exists("03341701-01_01.step"):
raise SystemExit("Uploaded STEP file missing: " + "03341701-01_01.step")
# Ensure FreeCAD can find user Mods (SheetMetal)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
sys.argv = ["stepanalyser.py", "--input", "03341701-01_01.step", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,470 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: --input <file.step|file.stp> (relative to cwd or absolute)
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- <basename>_flat.dxf
- <basename>_result.json
- <basename>_debug.FCStd
Additionally (compat):
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import json
import argparse
import traceback
from datetime import datetime
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
def mm2_to_m2(x: float) -> float:
return x / 1_000_000.0
def mm_to_m(x: float) -> float:
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation:
- cluster planar faces by normal direction
- estimate thickness from plane offsets
- fallback to distToShape on face pairs
"""
import math
shape = part_obj.Shape
def norm(v):
l = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
eps = 1e-9
x, y, z = float(n.x), float(n.y), float(n.z)
if abs(x) > eps:
return n if x > 0 else n.multiply(-1)
if abs(y) > eps:
return n if y > 0 else n.multiply(-1)
if abs(z) > eps:
return n if z > 0 else n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
u = (u0 + u1) * 0.5
v = (v0 + v1) * 0.5
return face.valueAt(u, v), face.normalAt(u, v)
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0:
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p))
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
cos_tol = math.cos(math.radians(2.0))
clusters = []
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0:
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i - 1])
add_candidate(gap)
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket) // 2]
if candidates:
return pick_mode(candidates, 0.01)
# fallback: distToShape (slower)
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i + 1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0]
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
def write_json(path, payload):
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
def try_copy(src, dst):
try:
if src != dst and os.path.exists(src):
# overwrite
with open(src, "rb") as fsrc:
data = fsrc.read()
with open(dst, "wb") as fdst:
fdst.write(data)
except Exception:
pass
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="STEP file path (.step/.stp), absolute or relative to cwd")
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = args.input
if not os.path.isabs(step_path):
step_path = os.path.join(cwd, step_path)
if not os.path.exists(step_path):
raise SystemExit(f"STEP file not found in working directory: {step_path}")
base = os.path.splitext(os.path.basename(step_path))[0]
out_dxf_named = os.path.join(cwd, f"{base}_flat.dxf")
out_json_named = os.path.join(cwd, f"{base}_result.json")
out_fcstd_named = os.path.join(cwd, f"{base}_debug.FCStd")
# compat outputs for the web UI
out_dxf = os.path.join(cwd, "flat.dxf")
out_json = os.path.join(cwd, "result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Input:", os.path.basename(step_path), flush=True)
print("Material:", material_key, "Density:", density, flush=True)
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception as e:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception as e:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available (V1/V2). Check SheetMetal installation.")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
if thickness_mm <= 0:
raise RuntimeError("Invalid thickness (<= 0)")
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed; trying V1 fallback.")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed (no unfolded shape or sketches).")
# Export DXF (named)
importDXF.export(sketches, out_dxf_named)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
"flat": {
"bbox_mm": {"width_mm": bbox_w, "height_mm": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_bbox_m2": area_bbox_m2,
"area_net_mm2": area_net_mm2,
"area_net_m2": area_net_m2,
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg,
"bbox_g": mass_bbox_kg * 1000.0,
"net_g": mass_net_kg * 1000.0,
},
"output": {
"dxf_named": os.path.abspath(out_dxf_named),
"json_named": os.path.abspath(out_json_named),
"fcstd_named": os.path.abspath(out_fcstd_named),
"dxf": os.path.abspath(out_dxf),
"json": os.path.abspath(out_json),
"fcstd": os.path.abspath(out_fcstd),
},
"warnings": warnings,
}
write_json(out_json_named, result)
# Save debug doc (named)
doc.saveAs(out_fcstd_named)
# Compat copies for web UI
try_copy(out_dxf_named, out_dxf)
try_copy(out_json_named, out_json)
try_copy(out_fcstd_named, out_fcstd)
print("OK: wrote DXF + JSON", flush=True)
except Exception as e:
# Always write named + compat error JSON
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": args.thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
}
try:
write_json(out_json_named, err)
except Exception:
pass
try:
write_json(out_json, err)
except Exception:
pass
try:
doc.saveAs(out_fcstd_named)
try_copy(out_fcstd_named, out_fcstd)
except Exception:
pass
print("ERROR:", str(e), flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
# Catch also SystemExit/argparse exits so we still emit result.json
try:
main()
except BaseException as e:
cwd = os.getcwd()
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
}
try:
with open(os.path.join(cwd, "result.json"), "w", encoding="utf-8") as f:
json.dump(err, f, indent=2, ensure_ascii=False)
except Exception:
pass
print("FATAL:", err["error_type"], err["error"], flush=True)
os._exit(1)

View File

@@ -1,6 +0,0 @@
{
"ok": false,
"error_type": "SystemExit",
"error": "STEP File not found not found in working directory",
"traceback": "Traceback (most recent call last):\n File \"/Users/christiananetzberger/development/stepanalyser_web/_jobs/8746e1ef90f5/run_stepanalyser.py\", line 28, in <module>\n exec(compile(code, \"stepanalyser.py\", \"exec\"), {\"__name__\": \"__main__\"})\n File \"stepanalyser.py\", line 417, in <module>\n main()\n File \"stepanalyser.py\", line 285, in main\n raise SystemExit(\"STEP File not found not found in working directory\")\nSystemExit: STEP File not found not found in working directory\n"
}

View File

@@ -1,3 +0,0 @@
=== STEPANALYSER START ===
Material: stainless Density: 8000.0
RUNNER ERROR: SystemExit STEP File not found not found in working directory

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(ok, payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
# Ensure FreeCAD can find user-installed Mods (SheetMetal etc.)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
# Also add SheetMetal folder explicitly (some setups need it)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
# argv injection
sys.argv = ["stepanalyser.py", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(False, payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,417 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: ./test.step
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import sys
import json
import argparse
import traceback
from datetime import datetime
# -----------------------------
# Configuration
# -----------------------------
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
# -----------------------------
# Helpers
# -----------------------------
def mm2_to_m2(x):
return x / 1_000_000.0
def mm_to_m(x):
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation for sheet-metal solids:
- Collect planar faces
- Cluster by (normalized) face normal direction
- For each cluster, compute plane offsets d = n·p for face points
- Thickness candidates are small positive differences between distinct d values
- Fallback: use distToShape on representative face pairs
Returns thickness in mm or None.
"""
import math
import Part
shape = part_obj.Shape
# --- helpers ---
def vec_to_tuple(v):
return (float(v.x), float(v.y), float(v.z))
def norm(v):
l = math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
"""
Make the normal direction canonical so +n and -n map consistently:
Flip so the first significant component is positive.
"""
# convert to tuple for easier checks
x, y, z = float(n.x), float(n.y), float(n.z)
# find first component with magnitude > eps
eps = 1e-9
if abs(x) > eps:
if x < 0: return n.multiply(-1)
elif abs(y) > eps:
if y < 0: return n.multiply(-1)
elif abs(z) > eps:
if z < 0: return n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
# cos(angle) = n1·n2
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
return face.valueAt((u0+u1)*0.5, (v0+v1)*0.5), face.normalAt((u0+u1)*0.5, (v0+v1)*0.5)
# --- collect planar faces ---
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0: # mm², ignore tiny faces
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p)) # plane offset for n·x = d
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
# --- cluster by normal direction ---
# Tolerance: within ~2 degrees
cos_tol = math.cos(math.radians(2.0))
clusters = [] # each: {"n": normal, "faces": [(area, face, d), ...]}
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
# --- build thickness candidates from d-values inside each cluster ---
# For a sheet, same-normal planes occur at (outer) and (inner) offsets,
# so distinct d-values differ ~thickness.
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0: # mm range guard (tune if needed)
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
# unique d-values with binning (0.01 mm)
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
# candidate: smallest positive gap between uniq planes
# Often thickness is the smallest meaningful separation.
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i-1])
add_candidate(gap)
# --- if candidates exist, pick most frequent bin (mode-ish) ---
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket)//2]
if candidates:
return pick_mode(candidates, 0.01)
# --- Fallback: distToShape between face pairs in same normal cluster ---
# Slower but can rescue cases where d-values are too noisy.
# We try only top clusters and top faces.
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
# compare each face to others in same cluster; minimal non-zero distance tends to thickness
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i+1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0] # returns (dist, pts, info)
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
# -----------------------------
# Main
# -----------------------------
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = os.path.join(cwd, "03341791-01_01.step")
out_dxf = os.path.join(cwd, "03341791-01_01.dxf")
out_json = os.path.join(cwd, "03341791-01_01-result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Material:", material_key, "Density:", density, flush=True)
if not os.path.exists(step_path):
raise SystemExit("STEP File not found not found in working directory")
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed")
importDXF.export(sketches, out_dxf)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR
},
"flat": {
"bbox_mm": {"width": bbox_w, "height": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_net_mm2": area_net_mm2
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg
},
"warnings": warnings
}
with open(out_json, "w") as f:
json.dump(result, f, indent=2)
doc.saveAs(out_fcstd)
print("OK: flat.dxf + result.json written", flush=True)
except Exception as e:
try:
doc.saveAs(out_fcstd)
except Exception:
pass
err = {
"ok": False,
"error": str(e),
"traceback": traceback.format_exc()
}
with open(out_json, "w") as f:
json.dump(err, f, indent=2)
print("ERROR:", e, flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
import os, sys, traceback
def p(*a): print(*a, flush=True)
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
sys.argv = ["stepanalyser.py", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except Exception:
p("RUNNER ERROR:")
p(traceback.format_exc())
finally:
os._exit(0)

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
import os, sys, traceback
def p(*a): print(*a, flush=True)
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
sys.argv = ["stepanalyser.py", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except Exception:
p("RUNNER ERROR:")
p(traceback.format_exc())
finally:
os._exit(0)

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
=== STEPANALYSER START ===
Material: stainless Density: 8000.0

View File

@@ -1,13 +0,0 @@
import os, sys, traceback
def p(*a): print(*a, flush=True)
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
sys.argv = ["stepanalyser.py", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except Exception:
p("RUNNER ERROR:")
p(traceback.format_exc())
finally:
os._exit(0)

View File

@@ -1,417 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: ./test.step
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import sys
import json
import argparse
import traceback
from datetime import datetime
# -----------------------------
# Configuration
# -----------------------------
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
# -----------------------------
# Helpers
# -----------------------------
def mm2_to_m2(x):
return x / 1_000_000.0
def mm_to_m(x):
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation for sheet-metal solids:
- Collect planar faces
- Cluster by (normalized) face normal direction
- For each cluster, compute plane offsets d = n·p for face points
- Thickness candidates are small positive differences between distinct d values
- Fallback: use distToShape on representative face pairs
Returns thickness in mm or None.
"""
import math
import Part
shape = part_obj.Shape
# --- helpers ---
def vec_to_tuple(v):
return (float(v.x), float(v.y), float(v.z))
def norm(v):
l = math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
"""
Make the normal direction canonical so +n and -n map consistently:
Flip so the first significant component is positive.
"""
# convert to tuple for easier checks
x, y, z = float(n.x), float(n.y), float(n.z)
# find first component with magnitude > eps
eps = 1e-9
if abs(x) > eps:
if x < 0: return n.multiply(-1)
elif abs(y) > eps:
if y < 0: return n.multiply(-1)
elif abs(z) > eps:
if z < 0: return n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
# cos(angle) = n1·n2
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
return face.valueAt((u0+u1)*0.5, (v0+v1)*0.5), face.normalAt((u0+u1)*0.5, (v0+v1)*0.5)
# --- collect planar faces ---
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0: # mm², ignore tiny faces
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p)) # plane offset for n·x = d
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
# --- cluster by normal direction ---
# Tolerance: within ~2 degrees
cos_tol = math.cos(math.radians(2.0))
clusters = [] # each: {"n": normal, "faces": [(area, face, d), ...]}
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
# --- build thickness candidates from d-values inside each cluster ---
# For a sheet, same-normal planes occur at (outer) and (inner) offsets,
# so distinct d-values differ ~thickness.
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0: # mm range guard (tune if needed)
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
# unique d-values with binning (0.01 mm)
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
# candidate: smallest positive gap between uniq planes
# Often thickness is the smallest meaningful separation.
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i-1])
add_candidate(gap)
# --- if candidates exist, pick most frequent bin (mode-ish) ---
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket)//2]
if candidates:
return pick_mode(candidates, 0.01)
# --- Fallback: distToShape between face pairs in same normal cluster ---
# Slower but can rescue cases where d-values are too noisy.
# We try only top clusters and top faces.
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
# compare each face to others in same cluster; minimal non-zero distance tends to thickness
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i+1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0] # returns (dist, pts, info)
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
# -----------------------------
# Main
# -----------------------------
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = os.path.join(cwd, "03341791-01_01.step")
out_dxf = os.path.join(cwd, "03341791-01_01.dxf")
out_json = os.path.join(cwd, "03341791-01_01-result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Material:", material_key, "Density:", density, flush=True)
if not os.path.exists(step_path):
raise SystemExit("STEP File not found not found in working directory")
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed")
importDXF.export(sketches, out_dxf)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR
},
"flat": {
"bbox_mm": {"width": bbox_w, "height": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_net_mm2": area_net_mm2
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg
},
"warnings": warnings
}
with open(out_json, "w") as f:
json.dump(result, f, indent=2)
doc.saveAs(out_fcstd)
print("OK: flat.dxf + result.json written", flush=True)
except Exception as e:
try:
doc.saveAs(out_fcstd)
except Exception:
pass
err = {
"ok": False,
"error": str(e),
"traceback": traceback.format_exc()
}
with open(out_json, "w") as f:
json.dump(err, f, indent=2)
print("ERROR:", e, flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:17:09",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01.step",
"step_filename": "03341791-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 18.0,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 2.0000000000001106,
"height_mm": 46.000000000000014
},
"area_bbox_mm2": 92.00000000000512,
"area_bbox_m2": 9.200000000000511e-05,
"area_net_mm2": 4172.328731565105,
"area_net_m2": 0.004172328731565105
},
"weight": {
"bbox_kg": 0.013248000000000735,
"net_kg": 0.6008153373453751,
"bbox_g": 13.248000000000735,
"net_g": 600.815337345375
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/debug_last.FCStd"
},
"warnings": []
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:17:09",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01.step",
"step_filename": "03341791-01_01.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 18.0,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 2.0000000000001106,
"height_mm": 46.000000000000014
},
"area_bbox_mm2": 92.00000000000512,
"area_bbox_m2": 9.200000000000511e-05,
"area_net_mm2": 4172.328731565105,
"area_net_m2": 0.004172328731565105
},
"weight": {
"bbox_kg": 0.013248000000000735,
"net_kg": 0.6008153373453751,
"bbox_g": 13.248000000000735,
"net_g": 600.815337345375
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/03341791-01_01_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/dd7c3c14fe98/debug_last.FCStd"
},
"warnings": []
}

View File

@@ -1,7 +0,0 @@
=== STEPANALYSER START ===
Input: 03341791-01_01.step
Material: stainless Density: 8000.0
Error: Failed to open library "/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib"! Error: dlopen(/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib, 0x0005): tried: '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file, not in dyld cache)!
Auto-detecting thickness...
Detected thickness: 18.000 mm
OK: wrote DXF + JSON

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
if not os.path.exists("03341791-01_01.step"):
raise SystemExit("Uploaded STEP file missing: " + "03341791-01_01.step")
# Ensure FreeCAD can find user Mods (SheetMetal)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
sys.argv = ["stepanalyser.py", "--input", "03341791-01_01.step", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,470 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: --input <file.step|file.stp> (relative to cwd or absolute)
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- <basename>_flat.dxf
- <basename>_result.json
- <basename>_debug.FCStd
Additionally (compat):
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import json
import argparse
import traceback
from datetime import datetime
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
def mm2_to_m2(x: float) -> float:
return x / 1_000_000.0
def mm_to_m(x: float) -> float:
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation:
- cluster planar faces by normal direction
- estimate thickness from plane offsets
- fallback to distToShape on face pairs
"""
import math
shape = part_obj.Shape
def norm(v):
l = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
eps = 1e-9
x, y, z = float(n.x), float(n.y), float(n.z)
if abs(x) > eps:
return n if x > 0 else n.multiply(-1)
if abs(y) > eps:
return n if y > 0 else n.multiply(-1)
if abs(z) > eps:
return n if z > 0 else n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
u = (u0 + u1) * 0.5
v = (v0 + v1) * 0.5
return face.valueAt(u, v), face.normalAt(u, v)
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0:
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p))
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
cos_tol = math.cos(math.radians(2.0))
clusters = []
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0:
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i - 1])
add_candidate(gap)
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket) // 2]
if candidates:
return pick_mode(candidates, 0.01)
# fallback: distToShape (slower)
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i + 1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0]
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
def write_json(path, payload):
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
def try_copy(src, dst):
try:
if src != dst and os.path.exists(src):
# overwrite
with open(src, "rb") as fsrc:
data = fsrc.read()
with open(dst, "wb") as fdst:
fdst.write(data)
except Exception:
pass
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="STEP file path (.step/.stp), absolute or relative to cwd")
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = args.input
if not os.path.isabs(step_path):
step_path = os.path.join(cwd, step_path)
if not os.path.exists(step_path):
raise SystemExit(f"STEP file not found in working directory: {step_path}")
base = os.path.splitext(os.path.basename(step_path))[0]
out_dxf_named = os.path.join(cwd, f"{base}_flat.dxf")
out_json_named = os.path.join(cwd, f"{base}_result.json")
out_fcstd_named = os.path.join(cwd, f"{base}_debug.FCStd")
# compat outputs for the web UI
out_dxf = os.path.join(cwd, "flat.dxf")
out_json = os.path.join(cwd, "result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Input:", os.path.basename(step_path), flush=True)
print("Material:", material_key, "Density:", density, flush=True)
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception as e:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception as e:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available (V1/V2). Check SheetMetal installation.")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
if thickness_mm <= 0:
raise RuntimeError("Invalid thickness (<= 0)")
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed; trying V1 fallback.")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed (no unfolded shape or sketches).")
# Export DXF (named)
importDXF.export(sketches, out_dxf_named)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
"flat": {
"bbox_mm": {"width_mm": bbox_w, "height_mm": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_bbox_m2": area_bbox_m2,
"area_net_mm2": area_net_mm2,
"area_net_m2": area_net_m2,
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg,
"bbox_g": mass_bbox_kg * 1000.0,
"net_g": mass_net_kg * 1000.0,
},
"output": {
"dxf_named": os.path.abspath(out_dxf_named),
"json_named": os.path.abspath(out_json_named),
"fcstd_named": os.path.abspath(out_fcstd_named),
"dxf": os.path.abspath(out_dxf),
"json": os.path.abspath(out_json),
"fcstd": os.path.abspath(out_fcstd),
},
"warnings": warnings,
}
write_json(out_json_named, result)
# Save debug doc (named)
doc.saveAs(out_fcstd_named)
# Compat copies for web UI
try_copy(out_dxf_named, out_dxf)
try_copy(out_json_named, out_json)
try_copy(out_fcstd_named, out_fcstd)
print("OK: wrote DXF + JSON", flush=True)
except Exception as e:
# Always write named + compat error JSON
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": args.thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
}
try:
write_json(out_json_named, err)
except Exception:
pass
try:
write_json(out_json, err)
except Exception:
pass
try:
doc.saveAs(out_fcstd_named)
try_copy(out_fcstd_named, out_fcstd)
except Exception:
pass
print("ERROR:", str(e), flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
# Catch also SystemExit/argparse exits so we still emit result.json
try:
main()
except BaseException as e:
cwd = os.getcwd()
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
}
try:
with open(os.path.join(cwd, "result.json"), "w", encoding="utf-8") as f:
json.dump(err, f, indent=2, ensure_ascii=False)
except Exception:
pass
print("FATAL:", err["error_type"], err["error"], flush=True)
os._exit(1)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:02:56",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test.step",
"step_filename": "test.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 2.3000000000000007,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 57.8000000000001,
"height_mm": 120.47698930976972
},
"area_bbox_mm2": 6963.569982104702,
"area_bbox_m2": 0.0069635699821047016,
"area_net_mm2": 14502.94311573923,
"area_net_m2": 0.01450294311573923
},
"weight": {
"bbox_kg": 0.12812968767072655,
"net_kg": 0.2668541533296019,
"bbox_g": 128.12968767072655,
"net_g": 266.8541533296019
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/debug_last.FCStd"
},
"warnings": []
}

View File

@@ -1,7 +0,0 @@
=== STEPANALYSER START ===
Input: test.step
Material: stainless Density: 8000.0
Error: Failed to open library "/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib"! Error: dlopen(/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib, 0x0005): tried: '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file), '/System/Library/Frameworks/3DconnexionNavlib.framework/3DconnexionNavlib' (no such file, not in dyld cache)!
Auto-detecting thickness...
Detected thickness: 2.300 mm
OK: wrote DXF + JSON

View File

@@ -1,41 +0,0 @@
import os, sys, json, traceback
def write_result(payload):
try:
with open("result.json", "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
except Exception:
pass
try:
base = os.path.dirname(os.path.abspath(__file__))
os.chdir(base)
if not os.path.exists("test.step"):
raise SystemExit("Uploaded STEP file missing: " + "test.step")
# Ensure FreeCAD can find user Mods (SheetMetal installed in user profile)
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
sys.path.append(mod_dir)
sm_dir = os.path.join(mod_dir, "SheetMetal")
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
sys.path.append(sm_dir)
sys.argv = ["stepanalyser.py", "--input", "test.step", "--material", "stainless"]
code = open("stepanalyser.py", "r", encoding="utf-8").read()
exec(compile(code, "stepanalyser.py", "exec"), {"__name__": "__main__"})
except BaseException as e:
payload = {
"ok": False,
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc()
}
write_result(payload)
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
finally:
os._exit(0)

View File

@@ -1,470 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Internal STEP sheet metal analyser
- Input: --input <file.step|file.stp> (relative to cwd or absolute)
- Unfold with K-factor = 0.5
- Auto-detect thickness if not provided
- Export:
- <basename>_flat.dxf
- <basename>_result.json
- <basename>_debug.FCStd
Additionally (compat):
- flat.dxf
- result.json
- debug_last.FCStd
"""
import os
import json
import argparse
import traceback
from datetime import datetime
K_FACTOR = 0.5
K_STANDARD = "ansi"
DENSITY_KG_M3 = {
"alu": 2700.0,
"aluminum": 2700.0,
"stainless": 8000.0,
"edelstahl": 8000.0,
"copper": 8960.0,
"kupfer": 8960.0,
}
def mm2_to_m2(x: float) -> float:
return x / 1_000_000.0
def mm_to_m(x: float) -> float:
return x / 1000.0
def pick_main_object(doc):
candidates = []
for obj in doc.Objects:
if hasattr(obj, "Shape") and obj.Shape:
try:
if obj.Shape.Solids:
candidates.append((float(obj.Shape.Volume), obj))
except Exception:
pass
if not candidates:
raise RuntimeError("No solid object found in STEP.")
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def pick_root_face_index(shape):
planar = []
all_faces = []
for i, face in enumerate(shape.Faces, start=1):
try:
area = float(face.Area)
except Exception:
area = 0.0
all_faces.append((area, i, face))
try:
surf = face.Surface
if surf and "plane" in surf.__class__.__name__.lower():
planar.append((area, i, face))
except Exception:
pass
if planar:
planar.sort(key=lambda x: x[0], reverse=True)
return planar[0][1], True
all_faces.sort(key=lambda x: x[0], reverse=True)
return all_faces[0][1], False
def compute_bbox_mm(shape):
bb = shape.BoundBox
return float(bb.XLength), float(bb.YLength)
def estimate_thickness_mm_from_solid(part_obj, max_faces=60):
"""
Robust thickness estimation:
- cluster planar faces by normal direction
- estimate thickness from plane offsets
- fallback to distToShape on face pairs
"""
import math
shape = part_obj.Shape
def norm(v):
l = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
if l <= 1e-12:
return None
return v.multiply(1.0 / l)
def canonical_normal(n):
eps = 1e-9
x, y, z = float(n.x), float(n.y), float(n.z)
if abs(x) > eps:
return n if x > 0 else n.multiply(-1)
if abs(y) > eps:
return n if y > 0 else n.multiply(-1)
if abs(z) > eps:
return n if z > 0 else n.multiply(-1)
return n
def angle_close(n1, n2, cos_tol):
return (n1.dot(n2) >= cos_tol)
def face_midpoint(face):
u0, u1, v0, v1 = face.ParameterRange
u = (u0 + u1) * 0.5
v = (v0 + v1) * 0.5
return face.valueAt(u, v), face.normalAt(u, v)
planar = []
for face in shape.Faces:
try:
surf = face.Surface
if not (surf and "plane" in surf.__class__.__name__.lower()):
continue
area = float(face.Area)
if area < 1.0:
continue
p, n = face_midpoint(face)
n = norm(n)
if n is None:
continue
n = canonical_normal(n)
d = float(n.dot(p))
planar.append((area, face, n, d))
except Exception:
continue
if not planar:
return None
planar.sort(key=lambda x: x[0], reverse=True)
planar = planar[:max_faces]
cos_tol = math.cos(math.radians(2.0))
clusters = []
for area, face, n, d in planar:
placed = False
for c in clusters:
if angle_close(n, c["n"], cos_tol):
c["faces"].append((area, face, d))
placed = True
break
if not placed:
clusters.append({"n": n, "faces": [(area, face, d)]})
candidates = []
def add_candidate(val):
if 0.05 <= val <= 20.0:
candidates.append(val)
for c in clusters:
ds = [d for _a, _f, d in c["faces"]]
if len(ds) < 2:
continue
ds.sort()
uniq = []
for d in ds:
b = round(d / 0.01) * 0.01
if not uniq or abs(b - uniq[-1]) > 1e-9:
uniq.append(b)
if len(uniq) < 2:
continue
for i in range(1, len(uniq)):
gap = abs(uniq[i] - uniq[i - 1])
add_candidate(gap)
def pick_mode(vals, bin_size=0.01):
bins = {}
for x in vals:
b = round(x / bin_size) * bin_size
bins.setdefault(b, []).append(x)
best_bin = max(bins.items(), key=lambda kv: len(kv[1]))[0]
bucket = sorted(bins[best_bin])
return bucket[len(bucket) // 2]
if candidates:
return pick_mode(candidates, 0.01)
# fallback: distToShape (slower)
try:
for c in clusters[:6]:
faces = sorted(c["faces"], key=lambda t: t[0], reverse=True)[:8]
for i in range(len(faces)):
fi = faces[i][1]
for j in range(i + 1, len(faces)):
fj = faces[j][1]
dist = fi.distToShape(fj)[0]
if dist and dist > 0.05 and dist <= 20.0:
candidates.append(float(dist))
if candidates:
return pick_mode(candidates, 0.01)
except Exception:
pass
return None
def write_json(path, payload):
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
def try_copy(src, dst):
try:
if src != dst and os.path.exists(src):
# overwrite
with open(src, "rb") as fsrc:
data = fsrc.read()
with open(dst, "wb") as fdst:
fdst.write(data)
except Exception:
pass
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="STEP file path (.step/.stp), absolute or relative to cwd")
parser.add_argument("--material", required=True, help="alu | stainless | copper")
parser.add_argument("--thickness-mm", required=False, type=float, default=None,
help="Optional sheet thickness in mm (auto-detect if omitted)")
args = parser.parse_args()
material_key = args.material.strip().lower()
if material_key not in DENSITY_KG_M3:
raise SystemExit(f"Unknown material '{args.material}'")
density = DENSITY_KG_M3[material_key]
cwd = os.getcwd()
step_path = args.input
if not os.path.isabs(step_path):
step_path = os.path.join(cwd, step_path)
if not os.path.exists(step_path):
raise SystemExit(f"STEP file not found in working directory: {step_path}")
base = os.path.splitext(os.path.basename(step_path))[0]
out_dxf_named = os.path.join(cwd, f"{base}_flat.dxf")
out_json_named = os.path.join(cwd, f"{base}_result.json")
out_fcstd_named = os.path.join(cwd, f"{base}_debug.FCStd")
# compat outputs for the web UI
out_dxf = os.path.join(cwd, "flat.dxf")
out_json = os.path.join(cwd, "result.json")
out_fcstd = os.path.join(cwd, "debug_last.FCStd")
print("=== STEPANALYSER START ===", flush=True)
print("Input:", os.path.basename(step_path), flush=True)
print("Material:", material_key, "Density:", density, flush=True)
import FreeCAD as App
import Import
import importDXF
try:
import SheetMetalNewUnfolder
from SheetMetalNewUnfolder import BendAllowanceCalculator
HAS_V2 = True
except Exception as e:
HAS_V2 = False
try:
import SheetMetalUnfolder
HAS_V1 = True
except Exception as e:
HAS_V1 = False
if not HAS_V1 and not HAS_V2:
raise SystemExit("No SheetMetal unfolder available (V1/V2). Check SheetMetal installation.")
doc = App.newDocument("StepAnalyser")
warnings = []
try:
Import.insert(step_path, doc.Name)
doc.recompute()
part_obj = pick_main_object(doc)
face_idx, planar = pick_root_face_index(part_obj.Shape)
base_face = f"Face{face_idx}"
thickness_mm = args.thickness_mm
if thickness_mm is None:
print("Auto-detecting thickness...", flush=True)
thickness_mm = estimate_thickness_mm_from_solid(part_obj)
if thickness_mm is None:
raise RuntimeError("Could not auto-detect thickness")
print(f"Detected thickness: {thickness_mm:.3f} mm", flush=True)
if thickness_mm <= 0:
raise RuntimeError("Invalid thickness (<= 0)")
unfolded_shape = None
sketches = []
if HAS_V2:
try:
bac = BendAllowanceCalculator.from_single_value(K_FACTOR, K_STANDARD)
sel_face, unfolded_shape, bend_lines, root_normal = \
SheetMetalNewUnfolder.getUnfold(bac, part_obj, base_face)
sketches = SheetMetalNewUnfolder.getUnfoldSketches(
"Unfold", sel_face, unfolded_shape, bend_lines,
root_normal, [], False, "#000080", "#c00000", "#ff5733"
)
except Exception:
warnings.append("V2 unfold failed; trying V1 fallback.")
if unfolded_shape is None and HAS_V1:
ktable = {1: K_FACTOR}
unfolded_shape, foldComp, norm, *_ = \
SheetMetalUnfolder.getUnfold(ktable, part_obj, base_face, K_STANDARD)
sketches = SheetMetalUnfolder.getUnfoldSketches(
"Unfold", unfolded_shape, foldComp.Edges,
norm, [], False, "#000080",
bendSketchColor="#ff5733", internalSketchColor="#c00000"
)
if unfolded_shape is None or not sketches:
raise RuntimeError("Unfold failed (no unfolded shape or sketches).")
# Export DXF (named)
importDXF.export(sketches, out_dxf_named)
bbox_w, bbox_h = compute_bbox_mm(unfolded_shape)
area_bbox_mm2 = bbox_w * bbox_h
area_net_mm2 = float(unfolded_shape.Area)
t_m = mm_to_m(thickness_mm)
area_bbox_m2 = mm2_to_m2(area_bbox_mm2)
area_net_m2 = mm2_to_m2(area_net_mm2)
mass_bbox_kg = area_bbox_m2 * t_m * density
mass_net_kg = area_net_m2 * t_m * density
result = {
"ok": True,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
"flat": {
"bbox_mm": {"width_mm": bbox_w, "height_mm": bbox_h},
"area_bbox_mm2": area_bbox_mm2,
"area_bbox_m2": area_bbox_m2,
"area_net_mm2": area_net_mm2,
"area_net_m2": area_net_m2,
},
"weight": {
"bbox_kg": mass_bbox_kg,
"net_kg": mass_net_kg,
"bbox_g": mass_bbox_kg * 1000.0,
"net_g": mass_net_kg * 1000.0,
},
"output": {
"dxf_named": os.path.abspath(out_dxf_named),
"json_named": os.path.abspath(out_json_named),
"fcstd_named": os.path.abspath(out_fcstd_named),
"dxf": os.path.abspath(out_dxf),
"json": os.path.abspath(out_json),
"fcstd": os.path.abspath(out_fcstd),
},
"warnings": warnings,
}
write_json(out_json_named, result)
# Save debug doc (named)
doc.saveAs(out_fcstd_named)
# Compat copies for web UI
try_copy(out_dxf_named, out_dxf)
try_copy(out_json_named, out_json)
try_copy(out_fcstd_named, out_fcstd)
print("OK: wrote DXF + JSON", flush=True)
except Exception as e:
# Always write named + compat error JSON
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
"input": {
"step_file": os.path.abspath(step_path),
"step_filename": os.path.basename(step_path),
"material": material_key,
"density_kg_m3": density,
"thickness_mm": args.thickness_mm,
"k_factor": K_FACTOR,
"k_standard": K_STANDARD,
},
}
try:
write_json(out_json_named, err)
except Exception:
pass
try:
write_json(out_json, err)
except Exception:
pass
try:
doc.saveAs(out_fcstd_named)
try_copy(out_fcstd_named, out_fcstd)
except Exception:
pass
print("ERROR:", str(e), flush=True)
print(traceback.format_exc(), flush=True)
os._exit(1)
os._exit(0)
if __name__ == "__main__":
# Catch also SystemExit/argparse exits so we still emit result.json
try:
main()
except BaseException as e:
cwd = os.getcwd()
err = {
"ok": False,
"timestamp": datetime.now().isoformat(timespec="seconds"),
"error_type": type(e).__name__,
"error": str(e),
"traceback": traceback.format_exc(),
}
try:
with open(os.path.join(cwd, "result.json"), "w", encoding="utf-8") as f:
json.dump(err, f, indent=2, ensure_ascii=False)
except Exception:
pass
print("FATAL:", err["error_type"], err["error"], flush=True)
os._exit(1)

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"ok": true,
"timestamp": "2026-01-22T20:02:56",
"input": {
"step_file": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test.step",
"step_filename": "test.step",
"material": "stainless",
"density_kg_m3": 8000.0,
"thickness_mm": 2.3000000000000007,
"k_factor": 0.5,
"k_standard": "ansi"
},
"flat": {
"bbox_mm": {
"width_mm": 57.8000000000001,
"height_mm": 120.47698930976972
},
"area_bbox_mm2": 6963.569982104702,
"area_bbox_m2": 0.0069635699821047016,
"area_net_mm2": 14502.94311573923,
"area_net_m2": 0.01450294311573923
},
"weight": {
"bbox_kg": 0.12812968767072655,
"net_kg": 0.2668541533296019,
"bbox_g": 128.12968767072655,
"net_g": 266.8541533296019
},
"output": {
"dxf_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test_flat.dxf",
"json_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test_result.json",
"fcstd_named": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/test_debug.FCStd",
"dxf": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/flat.dxf",
"json": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/result.json",
"fcstd": "/Users/christiananetzberger/development/stepanalyser_web/_jobs/fc30340c6901/debug_last.FCStd"
},
"warnings": []
}