Dockerized
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
_jobs
|
||||||
|
.git
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
*.FCStd
|
||||||
|
*.dxf
|
||||||
|
*.svg
|
||||||
|
*.json
|
||||||
94
.gitignore
vendored
94
.gitignore
vendored
@@ -1,3 +1,15 @@
|
|||||||
|
# =========================
|
||||||
|
# OS / Editor
|
||||||
|
# =========================
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Python
|
# Python
|
||||||
# =========================
|
# =========================
|
||||||
@@ -5,55 +17,81 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
env/
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
pip-wheel-metadata/
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
coverage/
|
||||||
|
.coverage*
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# macOS
|
# Logs / runtime
|
||||||
# =========================
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# IDEs / Editors
|
|
||||||
# =========================
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# Logs
|
|
||||||
# =========================
|
# =========================
|
||||||
*.log
|
*.log
|
||||||
|
*.pid
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Job runtime data (generated)
|
# App runtime data (IMPORTANT)
|
||||||
# =========================
|
# =========================
|
||||||
|
# Job artifacts: uploads, results, dxf/svg previews, logs etc.
|
||||||
_jobs/
|
_jobs/
|
||||||
_jobs/**
|
_jobs/**
|
||||||
|
|
||||||
|
# If you keep a local config volume for dev
|
||||||
|
_config/
|
||||||
|
_config/**
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Generated outputs
|
# CAD / output files
|
||||||
# =========================
|
# =========================
|
||||||
*.dxf
|
*.dxf
|
||||||
*.svg
|
*.svg
|
||||||
*.json
|
*.step
|
||||||
*.FCStd
|
*.stp
|
||||||
|
*.iges
|
||||||
|
*.igs
|
||||||
|
*.stl
|
||||||
|
*.obj
|
||||||
|
|
||||||
# If you want to keep example outputs, comment out the lines above selectively
|
# If you want to keep a sample file, commit it explicitly with:
|
||||||
|
# git add -f test.step
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Temporary files
|
# Docker
|
||||||
# =========================
|
# =========================
|
||||||
*.tmp
|
# Local overrides
|
||||||
*.bak
|
docker-compose.override.yml
|
||||||
*.old
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# OS / misc
|
# Node (if you ever add frontend tooling)
|
||||||
# =========================
|
# =========================
|
||||||
Thumbs.db
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Secrets (safety)
|
||||||
|
# =========================
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
FROM mambaorg/micromamba:1.5.10
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install FreeCAD + Python via conda-forge (headless)
|
||||||
|
RUN micromamba create -y -n fc -c conda-forge \
|
||||||
|
python=3.12 freecad \
|
||||||
|
&& micromamba clean -a -y
|
||||||
|
|
||||||
|
# Activate env for subsequent RUN/CMD
|
||||||
|
ENV MAMBA_DOCKERFILE_ACTIVATE=1
|
||||||
|
SHELL ["/bin/bash", "-lc"]
|
||||||
|
|
||||||
|
# Install small OS deps (git for SheetMetal)
|
||||||
|
# NOTE: do NOT switch users; keep it simple and reproducible
|
||||||
|
USER root
|
||||||
|
RUN mkdir -p /var/lib/apt/lists/partial \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends git ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Python deps into conda env
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN micromamba run -n fc pip install --no-cache-dir -r /app/requirements.txt
|
||||||
|
|
||||||
|
# App sources
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# SheetMetal workbench (we'll link it into HOME at runtime)
|
||||||
|
RUN mkdir -p /opt/freecad_mods \
|
||||||
|
&& git clone --depth 1 https://github.com/shaise/FreeCAD_SheetMetal.git /opt/freecad_mods/SheetMetal
|
||||||
|
|
||||||
|
# Make FreeCAD find SheetMetal without any host volume
|
||||||
|
ENV HOME=/opt/freecad_home
|
||||||
|
RUN mkdir -p /opt/freecad_home/.local/share/FreeCAD/Mod \
|
||||||
|
&& ln -s /opt/freecad_mods/SheetMetal /opt/freecad_home/.local/share/FreeCAD/Mod/SheetMetal
|
||||||
|
|
||||||
|
# Runtime env (overridable)
|
||||||
|
ENV FREECADCMD=/opt/conda/envs/fc/bin/freecadcmd
|
||||||
|
ENV FREECAD_TIMEOUT_SEC=1200
|
||||||
|
ENV FREECAD_LOCK_FILE=/app/_jobs/.freecad.lock
|
||||||
|
ENV HOME=/config
|
||||||
|
|
||||||
|
EXPOSE 5055
|
||||||
|
|
||||||
|
CMD ["bash", "-lc", "micromamba run -n fc gunicorn -w 1 --threads 4 -b 0.0.0.0:5055 app:app"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Internes Tool zur Kalkulation von Blechteilen.
|
||||||
|
|
||||||
|
Derzeit keine Unterstützung von Baugruppen!
|
||||||
|
|||||||
200
app.py
200
app.py
@@ -4,33 +4,37 @@ import uuid
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import fcntl
|
||||||
|
|
||||||
from flask import Flask, render_template, request, abort, Response
|
from flask import Flask, render_template, request, abort, Response
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
import ezdxf
|
import ezdxf
|
||||||
from ezdxf.addons.drawing import Frontend, RenderContext, svg, layout, config
|
from ezdxf.addons.drawing import Frontend, RenderContext, svg, layout, config as ezconfig
|
||||||
|
|
||||||
|
|
||||||
APP_DIR = Path(__file__).resolve().parent
|
APP_DIR = Path(__file__).resolve().parent
|
||||||
WORKER_DIR = APP_DIR / "worker"
|
WORKER_DIR = APP_DIR / "worker"
|
||||||
|
|
||||||
FREECADCMD = "/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd"
|
|
||||||
|
|
||||||
JOBS_DIR = APP_DIR / "_jobs"
|
JOBS_DIR = APP_DIR / "_jobs"
|
||||||
JOBS_DIR.mkdir(exist_ok=True)
|
JOBS_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# In Docker: /usr/bin/freecadcmd, lokal (mac): via env setzen, wenn du willst
|
||||||
|
FREECAD_TIMEOUT_SEC = int(os.environ.get("FREECAD_TIMEOUT_SEC", "1200"))
|
||||||
|
FREECADCMD = (os.environ.get("FREECADCMD") or "").strip() or "/opt/conda/envs/fc/bin/freecadcmd"
|
||||||
|
|
||||||
ALLOWED_MATERIALS = [
|
ALLOWED_MATERIALS = [
|
||||||
("stainless", "Edelstahl"),
|
("stainless", "Edelstahl"),
|
||||||
("alu", "Aluminium"),
|
("alu", "Aluminium"),
|
||||||
("copper", "Kupfer"),
|
("copper", "Kupfer"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Serialize FreeCAD runs (one at a time)
|
||||||
|
LOCK_FILE = os.environ.get("FREECAD_LOCK_FILE", str(JOBS_DIR / ".freecad.lock"))
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _safe_job_dir(job_id: str) -> Path:
|
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()):
|
if not job_id or any(c not in "0123456789abcdef" for c in job_id.lower()):
|
||||||
abort(404)
|
abort(404)
|
||||||
job_dir = (JOBS_DIR / job_id).resolve()
|
job_dir = (JOBS_DIR / job_id).resolve()
|
||||||
@@ -41,13 +45,31 @@ def _safe_job_dir(job_id: str) -> Path:
|
|||||||
return job_dir
|
return job_dir
|
||||||
|
|
||||||
|
|
||||||
|
class FreeCADLock:
|
||||||
|
def __init__(self, lock_path: str):
|
||||||
|
self.lock_path = lock_path
|
||||||
|
self.fp = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.fp = open(self.lock_path, "w")
|
||||||
|
fcntl.flock(self.fp, fcntl.LOCK_EX)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
try:
|
||||||
|
fcntl.flock(self.fp, fcntl.LOCK_UN)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self.fp.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def run_job(job_dir: Path, filename: str, material: str, thickness_mm: str | None):
|
def run_job(job_dir: Path, filename: str, material: str, thickness_mm: str | None):
|
||||||
"""
|
"""
|
||||||
Runs FreeCAD headless in a job dir. Keeps original filename.
|
Runs FreeCAD headless in a job dir. Keeps original filename.
|
||||||
Expects: job_dir/filename exists.
|
Produces: job_dir/result.json + flat.dxf (compat), plus named outputs by analyser.
|
||||||
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")
|
shutil.copy2(WORKER_DIR / "stepanalyser.py", job_dir / "stepanalyser.py")
|
||||||
|
|
||||||
argv = ['"stepanalyser.py"', '"--input"', f'"{filename}"', '"--material"', f'"{material}"']
|
argv = ['"stepanalyser.py"', '"--input"', f'"{filename}"', '"--material"', f'"{material}"']
|
||||||
@@ -58,7 +80,7 @@ def run_job(job_dir: Path, filename: str, material: str, thickness_mm: str | Non
|
|||||||
|
|
||||||
def write_result(payload):
|
def write_result(payload):
|
||||||
try:
|
try:
|
||||||
with open("result.json", "w", encoding="utf-8") as f:
|
with open(\"result.json\", \"w\", encoding=\"utf-8\") as f:
|
||||||
json.dump(payload, f, indent=2, ensure_ascii=False)
|
json.dump(payload, f, indent=2, ensure_ascii=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -68,87 +90,143 @@ try:
|
|||||||
os.chdir(base)
|
os.chdir(base)
|
||||||
|
|
||||||
if not os.path.exists({json.dumps(filename)}):
|
if not os.path.exists({json.dumps(filename)}):
|
||||||
raise SystemExit("Uploaded STEP file missing: " + {json.dumps(filename)})
|
raise SystemExit(\"Uploaded STEP file missing: \" + {json.dumps(filename)})
|
||||||
|
|
||||||
# Ensure FreeCAD can find user Mods (SheetMetal)
|
# Ensure FreeCAD can find user Mods (SheetMetal)
|
||||||
mod_dir = os.path.expanduser("~/Library/Application Support/FreeCAD/Mod")
|
# Linux default: ~/.local/share/FreeCAD/Mod
|
||||||
|
mod_dir = os.path.expanduser("~/.local/share/FreeCAD/Mod")
|
||||||
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
|
if os.path.isdir(mod_dir) and mod_dir not in sys.path:
|
||||||
sys.path.append(mod_dir)
|
sys.path.append(mod_dir)
|
||||||
sm_dir = os.path.join(mod_dir, "SheetMetal")
|
sm_dir = os.path.join(mod_dir, \"SheetMetal\")
|
||||||
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
|
if os.path.isdir(sm_dir) and sm_dir not in sys.path:
|
||||||
sys.path.append(sm_dir)
|
sys.path.append(sm_dir)
|
||||||
|
|
||||||
sys.argv = [{", ".join(argv)}]
|
sys.argv = [{", ".join(argv)}]
|
||||||
|
|
||||||
code = open("stepanalyser.py", "r", encoding="utf-8").read()
|
code = open(\"stepanalyser.py\", \"r\", encoding=\"utf-8\").read()
|
||||||
exec(compile(code, "stepanalyser.py", "exec"), {{"__name__": "__main__"}})
|
exec(compile(code, \"stepanalyser.py\", \"exec\"), {{\"__name__\": \"__main__\"}})
|
||||||
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
payload = {{
|
payload = {{
|
||||||
"ok": False,
|
\"ok\": False,
|
||||||
"error_type": type(e).__name__,
|
\"error_type\": type(e).__name__,
|
||||||
"error": str(e),
|
\"error\": str(e),
|
||||||
"traceback": traceback.format_exc()
|
\"traceback\": traceback.format_exc()
|
||||||
}}
|
}}
|
||||||
write_result(payload)
|
write_result(payload)
|
||||||
print("RUNNER ERROR:", payload["error_type"], payload["error"], flush=True)
|
print(\"RUNNER ERROR:\", payload[\"error_type\"], payload[\"error\"], flush=True)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
(job_dir / "run_stepanalyser.py").write_text(runner_text, encoding="utf-8")
|
(job_dir / "run_stepanalyser.py").write_text(runner_text, encoding="utf-8")
|
||||||
|
|
||||||
cmd = [FREECADCMD, "run_stepanalyser.py"]
|
cmd = [FREECADCMD, "run_stepanalyser.py"]
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["HOME"] = str(Path.home())
|
# In linuxserver containers the writable, persistent home is /config
|
||||||
|
env["HOME"] = env.get("HOME") or "/config"
|
||||||
|
env["HOME"] = "/config" # enforce for consistent expanduser() + Mod discovery
|
||||||
|
|
||||||
proc = subprocess.run(
|
# Headless Qt hints (works on Debian-based hosts too)
|
||||||
cmd,
|
env.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
cwd=str(job_dir),
|
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
timeout=180,
|
|
||||||
env=env,
|
|
||||||
)
|
|
||||||
|
|
||||||
log_text = proc.stdout or ""
|
timeout = FREECAD_TIMEOUT_SEC
|
||||||
(job_dir / "run.log").write_text(log_text, encoding="utf-8")
|
|
||||||
|
|
||||||
|
log_path = job_dir / "run.log"
|
||||||
result_path = job_dir / "result.json"
|
result_path = job_dir / "result.json"
|
||||||
|
|
||||||
|
# One FreeCAD run at a time
|
||||||
|
with FreeCADLock(LOCK_FILE):
|
||||||
|
with open(log_path, "w", encoding="utf-8") as log_fp:
|
||||||
|
log_fp.write("=== STEPANALYSER START ===\n")
|
||||||
|
log_fp.write(f"Command: {cmd}\n")
|
||||||
|
log_fp.flush()
|
||||||
|
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=str(job_dir),
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=log_fp,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=timeout)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
# Kill the process and persist a useful error result
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
log_fp.write(f"\nTIMEOUT: FreeCAD run exceeded {timeout} seconds and was killed.\n")
|
||||||
|
log_fp.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tail = ""
|
||||||
|
try:
|
||||||
|
tail = log_path.read_text(encoding="utf-8")[-8000:]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
err = {
|
||||||
|
"ok": False,
|
||||||
|
"error_type": "TimeoutExpired",
|
||||||
|
"error": f"FreeCAD job timed out after {timeout} seconds",
|
||||||
|
"log_tail": tail,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
result_path.write_text(json.dumps(err, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
err["_job"] = {
|
||||||
|
"id": job_dir.name,
|
||||||
|
"dir": str(job_dir),
|
||||||
|
"exit_code": None,
|
||||||
|
"log_file": str(log_path),
|
||||||
|
"compat_dxf": str(job_dir / "flat.dxf"),
|
||||||
|
"compat_json": str(result_path),
|
||||||
|
"svg_preview_url": f"/job/{job_dir.name}/dxf.svg",
|
||||||
|
"dxf_download_url": f"/job/{job_dir.name}/flat.dxf",
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
|
||||||
|
# Normal completion path
|
||||||
|
exit_code = proc.returncode
|
||||||
|
|
||||||
if result_path.exists():
|
if result_path.exists():
|
||||||
result = json.loads(result_path.read_text(encoding="utf-8"))
|
result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||||
else:
|
else:
|
||||||
|
tail = ""
|
||||||
|
try:
|
||||||
|
tail = log_path.read_text(encoding="utf-8")[-4000:]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
result = {
|
result = {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"error_type": "RunnerError",
|
"error_type": "RunnerError",
|
||||||
"error": "result.json not created",
|
"error": "result.json not created",
|
||||||
"log_tail": log_text[-4000:],
|
"log_tail": tail,
|
||||||
}
|
}
|
||||||
|
|
||||||
result["_job"] = {
|
result["_job"] = {
|
||||||
"id": job_dir.name,
|
"id": job_dir.name,
|
||||||
"dir": str(job_dir),
|
"dir": str(job_dir),
|
||||||
"exit_code": proc.returncode,
|
"exit_code": exit_code,
|
||||||
"log_file": str(job_dir / "run.log"),
|
"log_file": str(log_path),
|
||||||
"compat_dxf": str(job_dir / "flat.dxf"),
|
"compat_dxf": str(job_dir / "flat.dxf"),
|
||||||
"compat_json": str(job_dir / "result.json"),
|
"compat_json": str(result_path),
|
||||||
"svg_preview_url": f"/job/{job_dir.name}/dxf.svg",
|
"svg_preview_url": f"/job/{job_dir.name}/dxf.svg",
|
||||||
"dxf_download_url": f"/job/{job_dir.name}/flat.dxf",
|
"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
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -156,39 +234,27 @@ def dxf_to_svg_bytes(dxf_path: Path) -> bytes:
|
|||||||
doc = ezdxf.readfile(str(dxf_path))
|
doc = ezdxf.readfile(str(dxf_path))
|
||||||
msp = doc.modelspace()
|
msp = doc.modelspace()
|
||||||
|
|
||||||
# 1) Context
|
|
||||||
context = RenderContext(doc)
|
context = RenderContext(doc)
|
||||||
|
|
||||||
# 2) Backend
|
|
||||||
backend = svg.SVGBackend()
|
backend = svg.SVGBackend()
|
||||||
|
|
||||||
# 3) Config: weißer Hintergrund + schwarz (super für Blechteile)
|
cfg = ezconfig.Configuration(
|
||||||
cfg = config.Configuration(
|
background_policy=ezconfig.BackgroundPolicy.WHITE,
|
||||||
background_policy=config.BackgroundPolicy.WHITE,
|
color_policy=ezconfig.ColorPolicy.BLACK,
|
||||||
color_policy=config.ColorPolicy.BLACK,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4) Frontend
|
|
||||||
frontend = Frontend(context, backend, config=cfg)
|
frontend = Frontend(context, backend, config=cfg)
|
||||||
|
|
||||||
# 5) Zeichnen
|
|
||||||
frontend.draw_layout(msp)
|
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))
|
page = layout.Page(0, 0, layout.Units.mm, margins=layout.Margins.all(2))
|
||||||
|
|
||||||
# Optional: fit to page (preview-geeignet)
|
|
||||||
try:
|
try:
|
||||||
settings = layout.Settings(scale=1, fit_page=True)
|
settings = layout.Settings(scale=1, fit_page=True)
|
||||||
svg_string = backend.get_string(page, settings=settings)
|
svg_string = backend.get_string(page, settings=settings)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# ältere ezdxf-Version ohne settings-Parameter
|
|
||||||
svg_string = backend.get_string(page)
|
svg_string = backend.get_string(page)
|
||||||
|
|
||||||
return svg_string.encode("utf-8")
|
return svg_string.encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html", materials=ALLOWED_MATERIALS)
|
return render_template("index.html", materials=ALLOWED_MATERIALS)
|
||||||
@@ -214,12 +280,13 @@ def analyse():
|
|||||||
job_dir = JOBS_DIR / job_id
|
job_dir = JOBS_DIR / job_id
|
||||||
job_dir.mkdir(parents=True, exist_ok=True)
|
job_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Save upload using original (safe) filename
|
|
||||||
step_dest = job_dir / filename
|
step_dest = job_dir / filename
|
||||||
f.save(str(step_dest))
|
f.save(str(step_dest))
|
||||||
|
|
||||||
thickness_mm = thickness if thickness else None
|
thickness_mm = thickness if thickness else None
|
||||||
result = run_job(job_dir, filename, material, thickness_mm)
|
result = run_job(job_dir, filename, material, thickness_mm)
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
result = {"ok": False, "error_type": "InternalError", "error": "Unexpected result type"}
|
||||||
|
|
||||||
return render_template("result.html", result=result)
|
return render_template("result.html", result=result)
|
||||||
|
|
||||||
@@ -248,16 +315,11 @@ def preview_dxf_svg(job_id: str):
|
|||||||
try:
|
try:
|
||||||
svg_bytes = dxf_to_svg_bytes(dxf_path)
|
svg_bytes = dxf_to_svg_bytes(dxf_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Return a simple SVG with an error message
|
|
||||||
msg = f"DXF->SVG failed: {type(e).__name__}: {e}"
|
msg = f"DXF->SVG failed: {type(e).__name__}: {e}"
|
||||||
svg = f"""<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="200">
|
svg_fallback = f"""<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="200">
|
||||||
<rect x="0" y="0" width="1200" height="200" fill="#111"/>
|
<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>
|
<text x="20" y="80" fill="#fff" font-family="system-ui, -apple-system" font-size="18">{msg}</text>
|
||||||
</svg>"""
|
</svg>"""
|
||||||
return Response(svg.encode("utf-8"), mimetype="image/svg+xml")
|
return Response(svg_fallback.encode("utf-8"), mimetype="image/svg+xml")
|
||||||
|
|
||||||
return Response(svg_bytes, 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)
|
|
||||||
|
|||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
stepanalyser:
|
||||||
|
build: .
|
||||||
|
container_name: stepanalyser
|
||||||
|
ports:
|
||||||
|
- "5055:5055"
|
||||||
|
volumes:
|
||||||
|
- ./_jobs:/app/_jobs
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
- FREECADCMD=/opt/conda/envs/fc/bin/freecadcmd
|
||||||
|
- FREECAD_TIMEOUT_SEC=1200
|
||||||
|
- FREECAD_LOCK_FILE=/app/_jobs/.freecad.lock
|
||||||
|
command: >
|
||||||
|
bash -lc '
|
||||||
|
mkdir -p /config/.local/share/FreeCAD/Mod &&
|
||||||
|
if [ ! -e /config/.local/share/FreeCAD/Mod/SheetMetal ]; then
|
||||||
|
ln -s /opt/freecad_mods/SheetMetal /config/.local/share/FreeCAD/Mod/SheetMetal;
|
||||||
|
fi &&
|
||||||
|
micromamba run -n fc gunicorn -w 1 --threads 4 -b 0.0.0.0:5055 app:app
|
||||||
|
'
|
||||||
|
restart: unless-stopped
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
flask==3.0.3
|
flask==3.0.3
|
||||||
|
gunicorn==22.0.0
|
||||||
ezdxf==1.3.4
|
ezdxf==1.3.4
|
||||||
pillow==10.4.0
|
pillow==10.4.0
|
||||||
Reference in New Issue
Block a user