This commit is contained in:
Christian Anetzberger
2026-01-22 20:23:51 +01:00
commit a197de9456
4327 changed files with 1235205 additions and 0 deletions

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

@@ -0,0 +1,38 @@
{
"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.

1994
_jobs/4699191e2667/flat.dxf Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
{
"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

@@ -0,0 +1,7 @@
=== 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

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,470 @@
#!/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)