initial
This commit is contained in:
BIN
_jobs/0e1d23a0181f/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/0e1d23a0181f/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
6
_jobs/0e1d23a0181f/result.json
Normal file
6
_jobs/0e1d23a0181f/result.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
3
_jobs/0e1d23a0181f/run.log
Normal file
3
_jobs/0e1d23a0181f/run.log
Normal file
@@ -0,0 +1,3 @@
|
||||
=== STEPANALYSER START ===
|
||||
Material: stainless Density: 8000.0
|
||||
RUNNER ERROR: SystemExit STEP File not found not found in working directory
|
||||
41
_jobs/0e1d23a0181f/run_stepanalyser.py
Normal file
41
_jobs/0e1d23a0181f/run_stepanalyser.py
Normal file
@@ -0,0 +1,41 @@
|
||||
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)
|
||||
417
_jobs/0e1d23a0181f/stepanalyser.py
Normal file
417
_jobs/0e1d23a0181f/stepanalyser.py
Normal file
@@ -0,0 +1,417 @@
|
||||
#!/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()
|
||||
1768
_jobs/0e1d23a0181f/test.step
Normal file
1768
_jobs/0e1d23a0181f/test.step
Normal file
File diff suppressed because it is too large
Load Diff
1768
_jobs/4699191e2667/03341701-01_01.step
Normal file
1768
_jobs/4699191e2667/03341701-01_01.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/4699191e2667/03341701-01_01_debug.FCStd
Normal file
BIN
_jobs/4699191e2667/03341701-01_01_debug.FCStd
Normal file
Binary file not shown.
1994
_jobs/4699191e2667/03341701-01_01_flat.dxf
Normal file
1994
_jobs/4699191e2667/03341701-01_01_flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/4699191e2667/03341701-01_01_result.json
Normal file
38
_jobs/4699191e2667/03341701-01_01_result.json
Normal 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": []
|
||||
}
|
||||
BIN
_jobs/4699191e2667/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/4699191e2667/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
BIN
_jobs/4699191e2667/debug_last.FCStd
Normal file
BIN
_jobs/4699191e2667/debug_last.FCStd
Normal file
Binary file not shown.
1994
_jobs/4699191e2667/flat.dxf
Normal file
1994
_jobs/4699191e2667/flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/4699191e2667/result.json
Normal file
38
_jobs/4699191e2667/result.json
Normal 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": []
|
||||
}
|
||||
7
_jobs/4699191e2667/run.log
Normal file
7
_jobs/4699191e2667/run.log
Normal 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
|
||||
41
_jobs/4699191e2667/run_stepanalyser.py
Normal file
41
_jobs/4699191e2667/run_stepanalyser.py
Normal 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)
|
||||
470
_jobs/4699191e2667/stepanalyser.py
Normal file
470
_jobs/4699191e2667/stepanalyser.py
Normal 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)
|
||||
1768
_jobs/5d7ecc58fcc3/03341701-01_01.step
Normal file
1768
_jobs/5d7ecc58fcc3/03341701-01_01.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/5d7ecc58fcc3/03341701-01_01_debug.FCStd
Normal file
BIN
_jobs/5d7ecc58fcc3/03341701-01_01_debug.FCStd
Normal file
Binary file not shown.
1994
_jobs/5d7ecc58fcc3/03341701-01_01_flat.dxf
Normal file
1994
_jobs/5d7ecc58fcc3/03341701-01_01_flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/5d7ecc58fcc3/03341701-01_01_result.json
Normal file
38
_jobs/5d7ecc58fcc3/03341701-01_01_result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
BIN
_jobs/5d7ecc58fcc3/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/5d7ecc58fcc3/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
BIN
_jobs/5d7ecc58fcc3/debug_last.FCStd
Normal file
BIN
_jobs/5d7ecc58fcc3/debug_last.FCStd
Normal file
Binary file not shown.
1994
_jobs/5d7ecc58fcc3/flat.dxf
Normal file
1994
_jobs/5d7ecc58fcc3/flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/5d7ecc58fcc3/result.json
Normal file
38
_jobs/5d7ecc58fcc3/result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
7
_jobs/5d7ecc58fcc3/run.log
Normal file
7
_jobs/5d7ecc58fcc3/run.log
Normal 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
|
||||
41
_jobs/5d7ecc58fcc3/run_stepanalyser.py
Normal file
41
_jobs/5d7ecc58fcc3/run_stepanalyser.py
Normal 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 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)
|
||||
470
_jobs/5d7ecc58fcc3/stepanalyser.py
Normal file
470
_jobs/5d7ecc58fcc3/stepanalyser.py
Normal 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)
|
||||
BIN
_jobs/79b91fe12e25/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/79b91fe12e25/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
BIN
_jobs/79b91fe12e25/debug_last.FCStd
Normal file
BIN
_jobs/79b91fe12e25/debug_last.FCStd
Normal file
Binary file not shown.
1882
_jobs/79b91fe12e25/flat.dxf
Normal file
1882
_jobs/79b91fe12e25/flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/79b91fe12e25/result.json
Normal file
38
_jobs/79b91fe12e25/result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
7
_jobs/79b91fe12e25/run.log
Normal file
7
_jobs/79b91fe12e25/run.log
Normal file
@@ -0,0 +1,7 @@
|
||||
=== 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
|
||||
41
_jobs/79b91fe12e25/run_stepanalyser.py
Normal file
41
_jobs/79b91fe12e25/run_stepanalyser.py
Normal 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("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)
|
||||
470
_jobs/79b91fe12e25/stepanalyser.py
Normal file
470
_jobs/79b91fe12e25/stepanalyser.py
Normal 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)
|
||||
1576
_jobs/79b91fe12e25/test.step
Normal file
1576
_jobs/79b91fe12e25/test.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/79b91fe12e25/test_debug.FCStd
Normal file
BIN
_jobs/79b91fe12e25/test_debug.FCStd
Normal file
Binary file not shown.
1882
_jobs/79b91fe12e25/test_flat.dxf
Normal file
1882
_jobs/79b91fe12e25/test_flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/79b91fe12e25/test_result.json
Normal file
38
_jobs/79b91fe12e25/test_result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
1768
_jobs/8693d05646e0/03341701-01_01.step
Normal file
1768
_jobs/8693d05646e0/03341701-01_01.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/8693d05646e0/03341701-01_01_debug.FCStd
Normal file
BIN
_jobs/8693d05646e0/03341701-01_01_debug.FCStd
Normal file
Binary file not shown.
1994
_jobs/8693d05646e0/03341701-01_01_flat.dxf
Normal file
1994
_jobs/8693d05646e0/03341701-01_01_flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/8693d05646e0/03341701-01_01_result.json
Normal file
38
_jobs/8693d05646e0/03341701-01_01_result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
BIN
_jobs/8693d05646e0/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/8693d05646e0/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
BIN
_jobs/8693d05646e0/debug_last.FCStd
Normal file
BIN
_jobs/8693d05646e0/debug_last.FCStd
Normal file
Binary file not shown.
1994
_jobs/8693d05646e0/flat.dxf
Normal file
1994
_jobs/8693d05646e0/flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/8693d05646e0/result.json
Normal file
38
_jobs/8693d05646e0/result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
7
_jobs/8693d05646e0/run.log
Normal file
7
_jobs/8693d05646e0/run.log
Normal 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
|
||||
41
_jobs/8693d05646e0/run_stepanalyser.py
Normal file
41
_jobs/8693d05646e0/run_stepanalyser.py
Normal 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)
|
||||
470
_jobs/8693d05646e0/stepanalyser.py
Normal file
470
_jobs/8693d05646e0/stepanalyser.py
Normal 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)
|
||||
BIN
_jobs/8746e1ef90f5/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/8746e1ef90f5/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
6
_jobs/8746e1ef90f5/result.json
Normal file
6
_jobs/8746e1ef90f5/result.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
3
_jobs/8746e1ef90f5/run.log
Normal file
3
_jobs/8746e1ef90f5/run.log
Normal file
@@ -0,0 +1,3 @@
|
||||
=== STEPANALYSER START ===
|
||||
Material: stainless Density: 8000.0
|
||||
RUNNER ERROR: SystemExit STEP File not found not found in working directory
|
||||
41
_jobs/8746e1ef90f5/run_stepanalyser.py
Normal file
41
_jobs/8746e1ef90f5/run_stepanalyser.py
Normal file
@@ -0,0 +1,41 @@
|
||||
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)
|
||||
417
_jobs/8746e1ef90f5/stepanalyser.py
Normal file
417
_jobs/8746e1ef90f5/stepanalyser.py
Normal file
@@ -0,0 +1,417 @@
|
||||
#!/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()
|
||||
1576
_jobs/8746e1ef90f5/test.step
Normal file
1576
_jobs/8746e1ef90f5/test.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/8cdde0e6adf4/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/8cdde0e6adf4/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
0
_jobs/8cdde0e6adf4/run.log
Normal file
0
_jobs/8cdde0e6adf4/run.log
Normal file
13
_jobs/8cdde0e6adf4/run_stepanalyser.py
Normal file
13
_jobs/8cdde0e6adf4/run_stepanalyser.py
Normal file
@@ -0,0 +1,13 @@
|
||||
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)
|
||||
0
_jobs/8cdde0e6adf4/stepanalyser.py
Normal file
0
_jobs/8cdde0e6adf4/stepanalyser.py
Normal file
1768
_jobs/8cdde0e6adf4/test.step
Normal file
1768
_jobs/8cdde0e6adf4/test.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/b45a697558e3/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/b45a697558e3/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
0
_jobs/b45a697558e3/run.log
Normal file
0
_jobs/b45a697558e3/run.log
Normal file
13
_jobs/b45a697558e3/run_stepanalyser.py
Normal file
13
_jobs/b45a697558e3/run_stepanalyser.py
Normal file
@@ -0,0 +1,13 @@
|
||||
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)
|
||||
0
_jobs/b45a697558e3/stepanalyser.py
Normal file
0
_jobs/b45a697558e3/stepanalyser.py
Normal file
1768
_jobs/b45a697558e3/test.step
Normal file
1768
_jobs/b45a697558e3/test.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/bb50fd78018d/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/bb50fd78018d/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
2
_jobs/bb50fd78018d/run.log
Normal file
2
_jobs/bb50fd78018d/run.log
Normal file
@@ -0,0 +1,2 @@
|
||||
=== STEPANALYSER START ===
|
||||
Material: stainless Density: 8000.0
|
||||
13
_jobs/bb50fd78018d/run_stepanalyser.py
Normal file
13
_jobs/bb50fd78018d/run_stepanalyser.py
Normal file
@@ -0,0 +1,13 @@
|
||||
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)
|
||||
417
_jobs/bb50fd78018d/stepanalyser.py
Normal file
417
_jobs/bb50fd78018d/stepanalyser.py
Normal file
@@ -0,0 +1,417 @@
|
||||
#!/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()
|
||||
1768
_jobs/bb50fd78018d/test.step
Normal file
1768
_jobs/bb50fd78018d/test.step
Normal file
File diff suppressed because it is too large
Load Diff
1942
_jobs/dd7c3c14fe98/03341791-01_01.step
Normal file
1942
_jobs/dd7c3c14fe98/03341791-01_01.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/dd7c3c14fe98/03341791-01_01_debug.FCStd
Normal file
BIN
_jobs/dd7c3c14fe98/03341791-01_01_debug.FCStd
Normal file
Binary file not shown.
2132
_jobs/dd7c3c14fe98/03341791-01_01_flat.dxf
Normal file
2132
_jobs/dd7c3c14fe98/03341791-01_01_flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/dd7c3c14fe98/03341791-01_01_result.json
Normal file
38
_jobs/dd7c3c14fe98/03341791-01_01_result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
BIN
_jobs/dd7c3c14fe98/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/dd7c3c14fe98/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
BIN
_jobs/dd7c3c14fe98/debug_last.FCStd
Normal file
BIN
_jobs/dd7c3c14fe98/debug_last.FCStd
Normal file
Binary file not shown.
2132
_jobs/dd7c3c14fe98/flat.dxf
Normal file
2132
_jobs/dd7c3c14fe98/flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/dd7c3c14fe98/result.json
Normal file
38
_jobs/dd7c3c14fe98/result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
7
_jobs/dd7c3c14fe98/run.log
Normal file
7
_jobs/dd7c3c14fe98/run.log
Normal file
@@ -0,0 +1,7 @@
|
||||
=== 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
|
||||
41
_jobs/dd7c3c14fe98/run_stepanalyser.py
Normal file
41
_jobs/dd7c3c14fe98/run_stepanalyser.py
Normal 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("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)
|
||||
470
_jobs/dd7c3c14fe98/stepanalyser.py
Normal file
470
_jobs/dd7c3c14fe98/stepanalyser.py
Normal 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)
|
||||
BIN
_jobs/fc30340c6901/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
BIN
_jobs/fc30340c6901/__pycache__/run_stepanalyser.cpython-311.pyc
Normal file
Binary file not shown.
BIN
_jobs/fc30340c6901/debug_last.FCStd
Normal file
BIN
_jobs/fc30340c6901/debug_last.FCStd
Normal file
Binary file not shown.
1882
_jobs/fc30340c6901/flat.dxf
Normal file
1882
_jobs/fc30340c6901/flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/fc30340c6901/result.json
Normal file
38
_jobs/fc30340c6901/result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
7
_jobs/fc30340c6901/run.log
Normal file
7
_jobs/fc30340c6901/run.log
Normal file
@@ -0,0 +1,7 @@
|
||||
=== 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
|
||||
41
_jobs/fc30340c6901/run_stepanalyser.py
Normal file
41
_jobs/fc30340c6901/run_stepanalyser.py
Normal 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("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)
|
||||
470
_jobs/fc30340c6901/stepanalyser.py
Normal file
470
_jobs/fc30340c6901/stepanalyser.py
Normal 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)
|
||||
1576
_jobs/fc30340c6901/test.step
Normal file
1576
_jobs/fc30340c6901/test.step
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_jobs/fc30340c6901/test_debug.FCStd
Normal file
BIN
_jobs/fc30340c6901/test_debug.FCStd
Normal file
Binary file not shown.
1882
_jobs/fc30340c6901/test_flat.dxf
Normal file
1882
_jobs/fc30340c6901/test_flat.dxf
Normal file
File diff suppressed because it is too large
Load Diff
38
_jobs/fc30340c6901/test_result.json
Normal file
38
_jobs/fc30340c6901/test_result.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
Reference in New Issue
Block a user