Background STL analysis with computing spinner; manual geometry override
- analyse_in_background() runs in FastAPI BackgroundTasks with its own DB session; _in_progress set prevents duplicate concurrent analyses - Job detail shows a spinner + auto-reloads every 3s while computing - 'Compute now' button also triggers background analysis - StlCache.manually_set flag (migration 0011): entries marked manual are never overwritten by auto-analysis - 'Override values' collapsible form on job detail lets user set bounding box and volume directly from slicer data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import uuid as _uuid
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import case, or_
|
||||
@@ -26,7 +26,7 @@ from app.estimation import estimate # also used in start_job
|
||||
from app.filamentdb import (available_colors, available_materials, find_matching_ams_roll,
|
||||
get_stock, log_print_on_roll, spool_counts)
|
||||
from app.stl import get_stl_dir, list_stl_files
|
||||
from app.stl_analysis import analyse, get_cache_map # noqa: F401 used in job_detail route
|
||||
from app.stl_analysis import analyse_in_background, get_cache_map, is_computing # noqa: F401
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
@@ -260,13 +260,14 @@ async def create_job(
|
||||
|
||||
|
||||
@router.get("/jobs/{job_id}", response_class=HTMLResponse)
|
||||
def job_detail(job_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
def job_detail(job_id: int, request: Request, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||
from fastapi import HTTPException
|
||||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
file_missing = False
|
||||
computing = False
|
||||
stl = None
|
||||
if job.file_name:
|
||||
file_path = get_stl_dir() / job.file_name
|
||||
@@ -275,7 +276,9 @@ def job_detail(job_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
else:
|
||||
stl = db.query(StlCache).filter_by(filename=job.file_name).first()
|
||||
if stl is None and not job.file_name.endswith(".gcode"):
|
||||
stl = analyse(job.file_name, db)
|
||||
if not is_computing(job.file_name):
|
||||
background_tasks.add_task(analyse_in_background, job.file_name)
|
||||
computing = True
|
||||
|
||||
estimation = estimate(stl.volume_ccm, job.job_material) if stl and stl.volume_ccm else None
|
||||
return templates.TemplateResponse("job_detail.html", {
|
||||
@@ -284,6 +287,7 @@ def job_detail(job_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
"job": job,
|
||||
"stl": stl,
|
||||
"file_missing": file_missing,
|
||||
"computing": computing,
|
||||
"estimation": estimation,
|
||||
"printers": db.query(Printer).order_by(Printer.name).all(),
|
||||
"brackets": db.query(JobBracket).order_by(JobBracket.name).all(),
|
||||
@@ -457,14 +461,47 @@ def finish_job(job_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/jobs/{job_id}/analyse")
|
||||
def analyse_job_file(job_id: int, db: Session = Depends(get_db)):
|
||||
def analyse_job_file(job_id: int, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||
from fastapi import HTTPException
|
||||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||||
if not job or not job.file_name:
|
||||
raise HTTPException(status_code=404, detail="Job or file not found")
|
||||
db.query(StlCache).filter_by(filename=job.file_name).delete()
|
||||
db.commit()
|
||||
analyse(job.file_name, db)
|
||||
background_tasks.add_task(analyse_in_background, job.file_name)
|
||||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post("/jobs/{job_id}/geometry")
|
||||
def set_geometry(
|
||||
job_id: int,
|
||||
volume_ccm: float = Form(...),
|
||||
cube_x: float = Form(...),
|
||||
cube_y: float = Form(...),
|
||||
cube_z: float = Form(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
from fastapi import HTTPException
|
||||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||||
if not job or not job.file_name:
|
||||
raise HTTPException(status_code=404, detail="Job or file not found")
|
||||
stl = db.query(StlCache).filter_by(filename=job.file_name).first()
|
||||
if stl:
|
||||
stl.volume_ccm = volume_ccm
|
||||
stl.cube_x = cube_x
|
||||
stl.cube_y = cube_y
|
||||
stl.cube_z = cube_z
|
||||
stl.manually_set = True
|
||||
else:
|
||||
db.add(StlCache(
|
||||
filename=job.file_name,
|
||||
volume_ccm=volume_ccm,
|
||||
cube_x=cube_x,
|
||||
cube_y=cube_y,
|
||||
cube_z=cube_z,
|
||||
manually_set=True,
|
||||
))
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user