diff --git a/alembic/versions/0007_stl_cache.py b/alembic/versions/0007_stl_cache.py new file mode 100644 index 0000000..8f5f231 --- /dev/null +++ b/alembic/versions/0007_stl_cache.py @@ -0,0 +1,35 @@ +"""stl_cache table for bounding box and volume + +Revision ID: 0007 +Revises: 0006 +Create Date: 2026-06-18 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0007" +down_revision: Union[str, None] = "0006" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "stl_cache", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("filename", sa.String(255), nullable=False), + sa.Column("cube_x", sa.Float(), nullable=True), + sa.Column("cube_y", sa.Float(), nullable=True), + sa.Column("cube_z", sa.Float(), nullable=True), + sa.Column("volume_ccm", sa.Float(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("filename"), + ) + + +def downgrade() -> None: + op.drop_table("stl_cache") diff --git a/app/models/__init__.py b/app/models/__init__.py index 26fe125..6fdcf4c 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -7,6 +7,7 @@ from app.models.print_job import PrintJob from app.models.printer import Printer from app.models.printer_log import PrinterLog from app.models.printer_type import PrinterType +from app.models.stl_cache import StlCache from app.models.todo import Todo -__all__ = ["Base", "Printer", "PrinterLog", "PrinterType", "Filament", "JobBracket", "JobLog", "PrintJob", "Todo", "PricingConfig"] +__all__ = ["Base", "Printer", "PrinterLog", "PrinterType", "Filament", "JobBracket", "JobLog", "PrintJob", "StlCache", "Todo", "PricingConfig"] diff --git a/app/models/stl_cache.py b/app/models/stl_cache.py new file mode 100644 index 0000000..bba6379 --- /dev/null +++ b/app/models/stl_cache.py @@ -0,0 +1,18 @@ +from datetime import datetime + +from sqlalchemy import DateTime, Float, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base + + +class StlCache(Base): + __tablename__ = "stl_cache" + + id: Mapped[int] = mapped_column(primary_key=True) + filename: Mapped[str] = mapped_column(String(255), unique=True) + cube_x: Mapped[float | None] = mapped_column(Float) # mm + cube_y: Mapped[float | None] = mapped_column(Float) # mm + cube_z: Mapped[float | None] = mapped_column(Float) # mm + volume_ccm: Mapped[float | None] = mapped_column(Float) # cm³ + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) diff --git a/app/routers/ui.py b/app/routers/ui.py index 25326a9..8dd07bd 100644 --- a/app/routers/ui.py +++ b/app/routers/ui.py @@ -19,6 +19,7 @@ from app.models.printer_type import PrinterType from app.models.pricing import PricingConfig from app.models.todo import Todo from app.stl import get_stl_dir, list_stl_files +from app.stl_analysis import analyse, get_cache_map router = APIRouter() templates = Jinja2Templates(directory="app/templates") @@ -137,7 +138,8 @@ def jobs_page(request: Request, q: str = "", db: Session = Depends(get_db)): "printers": db.query(Printer).order_by(Printer.name).all(), "filaments": db.query(Filament).order_by(Filament.material, Filament.color).all(), "brackets": db.query(JobBracket).order_by(JobBracket.name).all(), - "stl_files": list_stl_files(), + "stl_files": (stl_files := list_stl_files()), + "stl_cache": get_cache_map(stl_files, db), }) @@ -156,6 +158,7 @@ async def create_job( if stl_file and stl_file.filename: filename = stl_file.filename (get_stl_dir() / filename).write_bytes(await stl_file.read()) + analyse(filename, db) elif stl_select: filename = stl_select @@ -210,7 +213,8 @@ def bracket_detail(bracket_id: int, request: Request, db: Session = Depends(get_ "active": "brackets", "bracket": bracket, "filaments": db.query(Filament).order_by(Filament.material, Filament.color).all(), - "stl_files": list_stl_files(), + "stl_files": (stl_files := list_stl_files()), + "stl_cache": get_cache_map(stl_files, db), }) @@ -228,6 +232,7 @@ async def add_jobs_to_bracket( if stl_file and stl_file.filename: filename = stl_file.filename (get_stl_dir() / filename).write_bytes(await stl_file.read()) + analyse(filename, db) elif stl_select: filename = stl_select diff --git a/app/stl_analysis.py b/app/stl_analysis.py new file mode 100644 index 0000000..e18e683 --- /dev/null +++ b/app/stl_analysis.py @@ -0,0 +1,47 @@ +import logging +from pathlib import Path + +from sqlalchemy.orm import Session + +from app.models.stl_cache import StlCache + +logger = logging.getLogger("pops") + +_ANALYSABLE = {".stl", ".3mf"} + + +def analyse(filename: str, db: Session) -> StlCache | None: + """Return cached analysis for filename, computing it if missing.""" + existing = db.query(StlCache).filter(StlCache.filename == filename).first() + if existing: + return existing + + from app.stl import get_stl_dir + filepath = get_stl_dir() / filename + if Path(filename).suffix.lower() not in _ANALYSABLE or not filepath.exists(): + return None + + try: + import trimesh + mesh = trimesh.load(str(filepath), force="mesh") + extents = mesh.extents # [size_x, size_y, size_z] in mm + volume_mm3 = abs(mesh.volume) # mm³ (negative for inverted normals) + entry = StlCache( + filename=filename, + cube_x=round(float(extents[0]), 2), + cube_y=round(float(extents[1]), 2), + cube_z=round(float(extents[2]), 2), + volume_ccm=round(volume_mm3 / 1000, 3), + ) + db.add(entry) + db.commit() + return entry + except Exception: + logger.exception("STL analysis failed for %s", filename) + return None + + +def get_cache_map(filenames: list[str], db: Session) -> dict[str, StlCache]: + """Bulk-fetch cached entries for a list of filenames.""" + rows = db.query(StlCache).filter(StlCache.filename.in_(filenames)).all() + return {r.filename: r for r in rows} diff --git a/app/templates/bracket_detail.html b/app/templates/bracket_detail.html index 62f7d74..29568bc 100644 --- a/app/templates/bracket_detail.html +++ b/app/templates/bracket_detail.html @@ -114,7 +114,10 @@ {% if stl_files %} {% else %}
No files in library yet.
diff --git a/app/templates/jobs.html b/app/templates/jobs.html index 9d13455..1b94e07 100644 --- a/app/templates/jobs.html +++ b/app/templates/jobs.html @@ -129,7 +129,8 @@ {% else %} diff --git a/requirements.txt b/requirements.txt index bf30a37..adb30c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pymysql==1.1.1 cryptography==44.0.3 jinja2==3.1.4 python-multipart==0.0.20 +trimesh==4.5.3