initial
This commit is contained in:
263
app.py
Normal file
263
app.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, render_template, request, abort, Response
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import ezdxf
|
||||
from ezdxf.addons.drawing import Frontend, RenderContext, svg, layout, config
|
||||
|
||||
|
||||
APP_DIR = Path(__file__).resolve().parent
|
||||
WORKER_DIR = APP_DIR / "worker"
|
||||
|
||||
FREECADCMD = "/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd"
|
||||
|
||||
JOBS_DIR = APP_DIR / "_jobs"
|
||||
JOBS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
ALLOWED_MATERIALS = [
|
||||
("stainless", "Edelstahl"),
|
||||
("alu", "Aluminium"),
|
||||
("copper", "Kupfer"),
|
||||
]
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def _safe_job_dir(job_id: str) -> Path:
|
||||
# minimal validation to avoid path traversal
|
||||
if not job_id or any(c not in "0123456789abcdef" for c in job_id.lower()):
|
||||
abort(404)
|
||||
job_dir = (JOBS_DIR / job_id).resolve()
|
||||
if not str(job_dir).startswith(str(JOBS_DIR.resolve())):
|
||||
abort(404)
|
||||
if not job_dir.exists():
|
||||
abort(404)
|
||||
return job_dir
|
||||
|
||||
|
||||
def run_job(job_dir: Path, filename: str, material: str, thickness_mm: str | None):
|
||||
"""
|
||||
Runs FreeCAD headless in a job dir. Keeps original filename.
|
||||
Expects: job_dir/filename exists.
|
||||
Produces: job_dir/result.json (compat), flat.dxf (compat), plus named outputs.
|
||||
"""
|
||||
# Copy worker script into job dir
|
||||
shutil.copy2(WORKER_DIR / "stepanalyser.py", job_dir / "stepanalyser.py")
|
||||
|
||||
argv = ['"stepanalyser.py"', '"--input"', f'"{filename}"', '"--material"', f'"{material}"']
|
||||
if thickness_mm:
|
||||
argv += ['"--thickness-mm"', f'"{thickness_mm}"']
|
||||
|
||||
runner_text = f"""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({json.dumps(filename)}):
|
||||
raise SystemExit("Uploaded STEP file missing: " + {json.dumps(filename)})
|
||||
|
||||
# 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 = [{", ".join(argv)}]
|
||||
|
||||
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)
|
||||
"""
|
||||
(job_dir / "run_stepanalyser.py").write_text(runner_text, encoding="utf-8")
|
||||
|
||||
cmd = [FREECADCMD, "run_stepanalyser.py"]
|
||||
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(Path.home())
|
||||
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
cwd=str(job_dir),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=180,
|
||||
env=env,
|
||||
)
|
||||
|
||||
log_text = proc.stdout or ""
|
||||
(job_dir / "run.log").write_text(log_text, encoding="utf-8")
|
||||
|
||||
result_path = job_dir / "result.json"
|
||||
if result_path.exists():
|
||||
result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||
else:
|
||||
result = {
|
||||
"ok": False,
|
||||
"error_type": "RunnerError",
|
||||
"error": "result.json not created",
|
||||
"log_tail": log_text[-4000:],
|
||||
}
|
||||
|
||||
result["_job"] = {
|
||||
"id": job_dir.name,
|
||||
"dir": str(job_dir),
|
||||
"exit_code": proc.returncode,
|
||||
"log_file": str(job_dir / "run.log"),
|
||||
"compat_dxf": str(job_dir / "flat.dxf"),
|
||||
"compat_json": str(job_dir / "result.json"),
|
||||
"svg_preview_url": f"/job/{job_dir.name}/dxf.svg",
|
||||
"dxf_download_url": f"/job/{job_dir.name}/flat.dxf",
|
||||
}
|
||||
|
||||
# If analyser wrote named outputs, try to attach them (best effort)
|
||||
try:
|
||||
if "output" in result and isinstance(result["output"], dict):
|
||||
result["_job"]["named_dxf"] = result["output"].get("dxf_named")
|
||||
result["_job"]["named_json"] = result["output"].get("json_named")
|
||||
result["_job"]["named_fcstd"] = result["output"].get("fcstd_named")
|
||||
result["_job"]["compat_fcstd"] = result["output"].get("fcstd")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def dxf_to_svg_bytes(dxf_path: Path) -> bytes:
|
||||
doc = ezdxf.readfile(str(dxf_path))
|
||||
msp = doc.modelspace()
|
||||
|
||||
# 1) Context
|
||||
context = RenderContext(doc)
|
||||
|
||||
# 2) Backend
|
||||
backend = svg.SVGBackend()
|
||||
|
||||
# 3) Config: weißer Hintergrund + schwarz (super für Blechteile)
|
||||
cfg = config.Configuration(
|
||||
background_policy=config.BackgroundPolicy.WHITE,
|
||||
color_policy=config.ColorPolicy.BLACK,
|
||||
)
|
||||
|
||||
# 4) Frontend
|
||||
frontend = Frontend(context, backend, config=cfg)
|
||||
|
||||
# 5) Zeichnen
|
||||
frontend.draw_layout(msp)
|
||||
|
||||
# 6) Page: auto-detect size (0,0) + kleine Ränder
|
||||
page = layout.Page(0, 0, layout.Units.mm, margins=layout.Margins.all(2))
|
||||
|
||||
# Optional: fit to page (preview-geeignet)
|
||||
try:
|
||||
settings = layout.Settings(scale=1, fit_page=True)
|
||||
svg_string = backend.get_string(page, settings=settings)
|
||||
except TypeError:
|
||||
# ältere ezdxf-Version ohne settings-Parameter
|
||||
svg_string = backend.get_string(page)
|
||||
|
||||
return svg_string.encode("utf-8")
|
||||
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
return render_template("index.html", materials=ALLOWED_MATERIALS)
|
||||
|
||||
|
||||
@app.post("/analyse")
|
||||
def analyse():
|
||||
f = request.files.get("stepfile")
|
||||
material = (request.form.get("material", "stainless") or "stainless").strip().lower()
|
||||
thickness = (request.form.get("thickness_mm", "") or "").strip()
|
||||
|
||||
if material not in {m for m, _ in ALLOWED_MATERIALS}:
|
||||
return "Invalid material", 400
|
||||
|
||||
if not f or not f.filename:
|
||||
return "Please upload a .step or .stp file", 400
|
||||
|
||||
filename = secure_filename(f.filename)
|
||||
if not filename.lower().endswith((".step", ".stp")):
|
||||
return "Please upload a .step or .stp file", 400
|
||||
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
job_dir = JOBS_DIR / job_id
|
||||
job_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save upload using original (safe) filename
|
||||
step_dest = job_dir / filename
|
||||
f.save(str(step_dest))
|
||||
|
||||
thickness_mm = thickness if thickness else None
|
||||
result = run_job(job_dir, filename, material, thickness_mm)
|
||||
|
||||
return render_template("result.html", result=result)
|
||||
|
||||
|
||||
@app.get("/job/<job_id>/flat.dxf")
|
||||
def download_flat_dxf(job_id: str):
|
||||
job_dir = _safe_job_dir(job_id)
|
||||
dxf_path = (job_dir / "flat.dxf")
|
||||
if not dxf_path.exists():
|
||||
abort(404)
|
||||
data = dxf_path.read_bytes()
|
||||
return Response(
|
||||
data,
|
||||
mimetype="application/dxf",
|
||||
headers={"Content-Disposition": f'inline; filename="{job_id}_flat.dxf"'},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/job/<job_id>/dxf.svg")
|
||||
def preview_dxf_svg(job_id: str):
|
||||
job_dir = _safe_job_dir(job_id)
|
||||
dxf_path = (job_dir / "flat.dxf")
|
||||
if not dxf_path.exists():
|
||||
abort(404)
|
||||
|
||||
try:
|
||||
svg_bytes = dxf_to_svg_bytes(dxf_path)
|
||||
except Exception as e:
|
||||
# Return a simple SVG with an error message
|
||||
msg = f"DXF->SVG failed: {type(e).__name__}: {e}"
|
||||
svg = f"""<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="200">
|
||||
<rect x="0" y="0" width="1200" height="200" fill="#111"/>
|
||||
<text x="20" y="80" fill="#fff" font-family="system-ui, -apple-system" font-size="18">{msg}</text>
|
||||
</svg>"""
|
||||
return Response(svg.encode("utf-8"), mimetype="image/svg+xml")
|
||||
|
||||
return Response(svg_bytes, mimetype="image/svg+xml")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=5055, debug=True)
|
||||
Reference in New Issue
Block a user