feat(orca): added orcaslicer preset

This commit is contained in:
2026-01-28 14:30:04 +01:00
parent 443ff04430
commit e9cca3daeb
8556 changed files with 455257 additions and 215 deletions

View File

@@ -1,19 +1,13 @@
import os
import shutil
import uuid
import logging
from fastapi import FastAPI, UploadFile, File, HTTPException
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
# Import custom modules
from config import settings
from slicer import slicer_service
from calculator import GCodeParser, QuoteCalculator
from api.routes import router as api_router
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("api")
logger = logging.getLogger("main")
app = FastAPI(title="Print Calculator API")
@@ -29,85 +23,53 @@ app.add_middleware(
# Ensure directories exist
os.makedirs(settings.TEMP_DIR, exist_ok=True)
class QuoteResponse(BaseModel):
printer: str
print_time_seconds: int
print_time_formatted: str
material_grams: float
cost: dict
notes: list[str] = []
# Include Router
app.include_router(api_router, prefix="/api")
def cleanup_files(files: list):
for f in files:
try:
if os.path.exists(f):
os.remove(f)
except Exception as e:
logger.warning(f"Failed to delete temp file {f}: {e}")
# Legacy endpoint redirect or basic handler if needed for backward compatibility
# The frontend likely calls /calculate/stl.
# We should probably keep the old route or instruct user to update frontend.
# But for this task, let's remap the old route to the new logic if possible,
# or just expose the new route.
# The user request said: "Creare api/routes.py ... @app.post('/api/quote')"
# So we are creating a new endpoint.
# Existing frontend might break?
# The context says: "Currently uses hardcoded... Objective is to render system flexible... Frontend: Angular 19"
# The user didn't explicitly ask to update the frontend, but the new API is at /api/quote.
# I will keep the old "/calculate/stl" endpoint support by forwarding it or duplicating logic if critical,
# OR I'll assume the user will handle frontend updates.
# Better: I will alias the old route to the new one if parameters allow,
# but the new one expects Form data with different names maybe?
# Old: `/calculate/stl` just expected a file.
# I'll enable a simplified version on the old route for backward compat using defaults.
def format_time(seconds: int) -> str:
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
if h > 0:
return f"{int(h)}h {int(m)}m"
return f"{int(m)}m {int(s)}s"
from fastapi import UploadFile, File
from api.routes import calculate_quote
@app.post("/calculate/stl", response_model=QuoteResponse)
async def calculate_from_stl(file: UploadFile = File(...)):
if not file.filename.lower().endswith(".stl"):
raise HTTPException(status_code=400, detail="Only .stl files are supported.")
# Unique ID for this request
req_id = str(uuid.uuid4())
input_filename = f"{req_id}.stl"
output_filename = f"{req_id}.gcode"
@app.post("/calculate/stl")
async def legacy_calculate(file: UploadFile = File(...)):
"""Legacy endpoint compatibility"""
# Call the new logic with defaults
resp = await calculate_quote(file=file)
if not resp.success:
from fastapi import HTTPException
raise HTTPException(status_code=500, detail=resp.error)
input_path = os.path.join(settings.TEMP_DIR, input_filename)
output_path = os.path.join(settings.TEMP_DIR, output_filename)
try:
# 1. Save Uploaded File
logger.info(f"Received request {req_id} for file: {file.filename}")
with open(input_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# 2. Slice
# slicer_service methods raise exceptions on failure
slicer_service.slice_stl(input_path, output_path)
# 3. Parse Results
stats = GCodeParser.parse_metadata(output_path)
if stats["print_time_seconds"] == 0 and stats["filament_weight_g"] == 0:
# Slicing likely failed or produced empty output without throwing error
raise HTTPException(status_code=500, detail="Slicing completed but no stats found. Check mesh validity.")
# 4. Calculate Costs
quote = QuoteCalculator.calculate(stats)
return {
"printer": "BambuLab A1 (Estimated)",
"print_time_seconds": stats["print_time_seconds"],
"print_time_formatted": format_time(stats["print_time_seconds"]),
"material_grams": stats["filament_weight_g"],
"cost": {
"material": quote["breakdown"]["material_cost"],
"machine": quote["breakdown"]["machine_cost"],
"energy": quote["breakdown"]["energy_cost"],
"markup": quote["breakdown"]["markup_amount"],
"total": quote["total_price"]
},
"notes": ["Estimation generated using OrcaSlicer headless."]
}
except Exception as e:
logger.error(f"Error processing request: {e}")
raise HTTPException(status_code=500, detail=str(e))
finally:
# Cleanup
cleanup_files([input_path, output_path])
# Map Check response to old format
data = resp.data
return {
"printer": data.get("printer", "Unknown"),
"print_time_seconds": data.get("print_time_seconds", 0),
"print_time_formatted": data.get("print_time_formatted", ""),
"material_grams": data.get("material_grams", 0.0),
"cost": data.get("cost", {}),
"notes": ["Generated via Dynamic Slicer (Legacy Endpoint)"]
}
@app.get("/health")
def health_check():
return {"status": "ok", "slicer": settings.SLICER_PATH}
return {"status": "ok", "slicer": settings.SLICER_PATH}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)