mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-07 06:31:22 +02:00
fix(labels): replace incorrect ams_30x15 preset with correct AMS holder sizes (#1426)
Reporter — the same person who originally requested the labels feature in #809 — discovered that the ams_30x15 preset's 30x15 mm dimension didn't actually fit any variant of the MakerWorld AMS Filament Label Holder (model 752566) it advertised. Two new presets replace it: - ams_holder_74x33 (74 x 33 mm) matches the printable label STL bundled in the MakerWorld project - ams_holder_75x55 (75 x 55 mm) fits the cardstock-insert variant the reporter validated on bench Both cross the 20 mm height threshold so they land in the roomy layout branch — swatch on the left, QR on the right, multi-line text (brand, material, hex code, spool ID) in the middle. The old 30x15 mm preset couldn't fit a QR code; the new ones do. No DB migration: the preset name was never persisted. Callers scripting the old ams_30x15 value get a clean 422 at the route's Literal validator with the new valid values listed. i18n: replaced inventory.labels.templates.ams.{label,hint} with amsHolderSmall and amsHolderLarge across all 8 locales with real translations; parity guard cleaned of the stale English-fallback cognate entries. Parity holds at 4856 leaves per locale. Tests: backend label renderer + integration tests cover both new presets; LabelTemplatePickerModal test updated for the 6-button grid and the new template value in the API-call assertion.
This commit is contained in:
@@ -37,7 +37,8 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["labels"])
|
||||
|
||||
_VALID_TEMPLATES: tuple[TemplateName, ...] = (
|
||||
"ams_30x15",
|
||||
"ams_holder_74x33",
|
||||
"ams_holder_75x55",
|
||||
"box_40x30",
|
||||
"box_62x29",
|
||||
"avery_5160",
|
||||
@@ -51,7 +52,14 @@ MAX_LABELS_PER_REQUEST = 500
|
||||
|
||||
class LabelRequest(BaseModel):
|
||||
spool_ids: list[int] = Field(..., min_length=1, max_length=MAX_LABELS_PER_REQUEST)
|
||||
template: Literal["ams_30x15", "box_40x30", "box_62x29", "avery_5160", "avery_l7160"]
|
||||
template: Literal[
|
||||
"ams_holder_74x33",
|
||||
"ams_holder_75x55",
|
||||
"box_40x30",
|
||||
"box_62x29",
|
||||
"avery_5160",
|
||||
"avery_l7160",
|
||||
]
|
||||
|
||||
|
||||
def _split_extra_colors(raw: str | None) -> list[str] | None:
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""PDF spool label rendering.
|
||||
|
||||
Five fixed templates:
|
||||
Six fixed templates:
|
||||
|
||||
- ``ams_30x15`` — 30×15 mm single label, fits the popular Makerworld AMS
|
||||
Filament Label Holder (model 752566). One label per page.
|
||||
- ``ams_holder_74x33`` — 74×33 mm single label, matches the printable label
|
||||
STL bundled with the Makerworld AMS Filament Label Holder (model 752566).
|
||||
Smaller variant — the visible window in the holder. One label per page.
|
||||
- ``ams_holder_75x55`` — 75×55 mm single label, fits the cardstock-insert
|
||||
variant of the same holder. Roomier — swatch + QR + full text column.
|
||||
- ``box_40x30`` — 40×30 mm single label, common DK/Brother roll size and a
|
||||
good fit for filament-bag/storage-bin labels (#809 follow-up). Roomy
|
||||
layout — swatch, QR, full text column with hex code.
|
||||
@@ -12,6 +15,10 @@ Five fixed templates:
|
||||
- ``avery_5160`` — US Letter sheet, 25.4×66.7 mm × 30 per sheet.
|
||||
- ``avery_l7160`` — A4 sheet, 38.1×63.5 mm × 21 per sheet.
|
||||
|
||||
The legacy ``ams_30x15`` preset (#809) was incorrect — the original 30×15 mm
|
||||
dimension didn't fit any documented variant of model 752566. Replaced by the
|
||||
two ``ams_holder_*`` presets above (#1426).
|
||||
|
||||
The renderer is decoupled from the Spool model: callers build a ``LabelData``
|
||||
list from whatever source (local DB, Spoolman, future) so the same code path
|
||||
works in both modes.
|
||||
@@ -34,7 +41,14 @@ from reportlab.lib.pagesizes import A4, letter
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen import canvas as rl_canvas
|
||||
|
||||
TemplateName = Literal["ams_30x15", "box_40x30", "box_62x29", "avery_5160", "avery_l7160"]
|
||||
TemplateName = Literal[
|
||||
"ams_holder_74x33",
|
||||
"ams_holder_75x55",
|
||||
"box_40x30",
|
||||
"box_62x29",
|
||||
"avery_5160",
|
||||
"avery_l7160",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -180,17 +194,17 @@ def _draw_label(c: rl_canvas.Canvas, x: float, y: float, w: float, h: float, dat
|
||||
|
||||
Two layouts, picked by available height:
|
||||
|
||||
- **Tight** (h < 20 mm — AMS holder): swatch on the left, three lines of
|
||||
text on the right (brand, material+subtype, big spool ID). No QR — at
|
||||
30×15 mm there is not enough horizontal room for swatch + text + QR
|
||||
without truncating away the user-need fields, and the AMS holder is an
|
||||
at-a-glance identifier where the spool ID is the killer field. The
|
||||
box-label and Avery templates carry the QR for the other use cases.
|
||||
- **Tight** (h < 20 mm): swatch on the left, three lines of text on the
|
||||
right (brand, material+subtype, big spool ID). No QR — at very small
|
||||
heights there is not enough horizontal room for swatch + text + QR
|
||||
without truncating away the user-need fields. Kept as the safety
|
||||
branch for any future ultra-small preset; the shipped templates all
|
||||
land in the roomy layout below.
|
||||
|
||||
- **Roomy** (h >= 20 mm — box label, Avery sheets): swatch on the left,
|
||||
QR on the right, multi-line text in the middle column. Large spool ID
|
||||
anchored at bottom-left under the swatch so it stays readable when the
|
||||
label is on a box on a shelf at arm's length.
|
||||
- **Roomy** (h >= 20 mm — AMS holder, box label, Avery sheets): swatch
|
||||
on the left, QR on the right, multi-line text in the middle column.
|
||||
Large spool ID anchored at bottom-left under the swatch so it stays
|
||||
readable at arm's length.
|
||||
"""
|
||||
pad = 1.2 * mm
|
||||
inner_x, inner_y = x + pad, y + pad
|
||||
@@ -223,7 +237,7 @@ def _draw_label_tight(
|
||||
pad: float,
|
||||
data: LabelData,
|
||||
) -> None:
|
||||
"""AMS-holder layout (e.g. 30×15 mm). Swatch + brand/material/hex/ID, no QR."""
|
||||
"""Tight layout (h < 20 mm). Swatch + brand/material/hex/ID, no QR."""
|
||||
swatch_w = min(inner_h, inner_w * 0.35)
|
||||
swatch_y = inner_y + (inner_h - swatch_w) / 2
|
||||
_draw_swatch(c, inner_x, swatch_y, swatch_w, swatch_w, data)
|
||||
@@ -365,7 +379,8 @@ def _draw_label_roomy(
|
||||
|
||||
# (label_w_mm, label_h_mm) for single-label-per-page templates.
|
||||
_SINGLE_LABEL_SIZES_MM: dict[str, tuple[float, float]] = {
|
||||
"ams_30x15": (30.0, 15.0),
|
||||
"ams_holder_74x33": (74.0, 33.0),
|
||||
"ams_holder_75x55": (75.0, 55.0),
|
||||
"box_40x30": (40.0, 30.0),
|
||||
"box_62x29": (62.0, 29.0),
|
||||
}
|
||||
|
||||
@@ -66,7 +66,13 @@ class TestLocalInventoryLabels:
|
||||
@pytest.mark.integration
|
||||
async def test_all_four_templates_succeed(self, async_client: AsyncClient, spool_factory):
|
||||
s = await spool_factory()
|
||||
for template in ("ams_30x15", "box_62x29", "avery_5160", "avery_l7160"):
|
||||
for template in (
|
||||
"ams_holder_74x33",
|
||||
"ams_holder_75x55",
|
||||
"box_62x29",
|
||||
"avery_5160",
|
||||
"avery_l7160",
|
||||
):
|
||||
resp = await async_client.post(
|
||||
"/api/v1/inventory/labels",
|
||||
json={"spool_ids": [s.id], "template": template},
|
||||
@@ -100,7 +106,7 @@ class TestLocalInventoryLabels:
|
||||
s = await spool_factory()
|
||||
resp = await async_client.post(
|
||||
"/api/v1/inventory/labels",
|
||||
json={"spool_ids": [s.id, 99999], "template": "ams_30x15"},
|
||||
json={"spool_ids": [s.id, 99999], "template": "ams_holder_74x33"},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
assert "99999" in resp.text
|
||||
|
||||
@@ -6,7 +6,14 @@ import pytest
|
||||
|
||||
from backend.app.services.label_renderer import LabelData, render_labels
|
||||
|
||||
ALL_TEMPLATES = ("ams_30x15", "box_40x30", "box_62x29", "avery_5160", "avery_l7160")
|
||||
ALL_TEMPLATES = (
|
||||
"ams_holder_74x33",
|
||||
"ams_holder_75x55",
|
||||
"box_40x30",
|
||||
"box_62x29",
|
||||
"avery_5160",
|
||||
"avery_l7160",
|
||||
)
|
||||
|
||||
|
||||
def _sample(spool_id: int = 1, **overrides) -> LabelData:
|
||||
@@ -58,7 +65,7 @@ def test_missing_optional_fields_does_not_crash():
|
||||
deeplink_url="https://example.test/inventory?spool=42",
|
||||
)
|
||||
]
|
||||
pdf = render_labels("ams_30x15", data)
|
||||
pdf = render_labels("ams_holder_74x33", data)
|
||||
assert pdf.startswith(b"%PDF")
|
||||
|
||||
|
||||
@@ -74,7 +81,7 @@ def test_long_strings_are_truncated_not_overflowed():
|
||||
long_brand = "A" * 200
|
||||
long_name = "B" * 300
|
||||
data = [_sample(brand=long_brand, name=long_name)]
|
||||
pdf = render_labels("ams_30x15", data)
|
||||
pdf = render_labels("ams_holder_74x33", data)
|
||||
assert pdf.startswith(b"%PDF")
|
||||
|
||||
|
||||
@@ -121,9 +128,10 @@ def _render_uncompressed(template, data):
|
||||
from backend.app.services.label_renderer import _draw_label # noqa: PLC0415
|
||||
|
||||
# Mirror the page-size choice from render_labels but force pageCompression=0.
|
||||
if template in ("ams_30x15", "box_40x30", "box_62x29"):
|
||||
if template in ("ams_holder_74x33", "ams_holder_75x55", "box_40x30", "box_62x29"):
|
||||
sizes = {
|
||||
"ams_30x15": (30.0, 15.0),
|
||||
"ams_holder_74x33": (74.0, 33.0),
|
||||
"ams_holder_75x55": (75.0, 55.0),
|
||||
"box_40x30": (40.0, 30.0),
|
||||
"box_62x29": (62.0, 29.0),
|
||||
}
|
||||
@@ -167,9 +175,9 @@ def _render_uncompressed(template, data):
|
||||
def test_ams_template_actually_renders_text():
|
||||
"""Regression: the first cut of the AMS-holder layout produced labels with
|
||||
only swatch + QR and no text at all because the side-by-side layout left
|
||||
<5 mm for the text column. The redesign drops the QR on this template and
|
||||
gives the right side to brand + material + spool ID. This pins that the
|
||||
rendered PDF contains all three fields.
|
||||
<5 mm for the text column. The current AMS templates use the roomy layout
|
||||
(swatch + QR + multi-line text); this pins that the rendered PDF contains
|
||||
brand + material + spool ID for the smaller AMS preset.
|
||||
"""
|
||||
data = [
|
||||
LabelData(
|
||||
@@ -182,7 +190,7 @@ def test_ams_template_actually_renders_text():
|
||||
deeplink_url="https://example.test/inventory?spool=42",
|
||||
)
|
||||
]
|
||||
pdf = _render_uncompressed("ams_30x15", data)
|
||||
pdf = _render_uncompressed("ams_holder_74x33", data)
|
||||
assert b"Polymaker" in pdf, "AMS template must render the brand"
|
||||
assert b"PLA" in pdf, "AMS template must render the material"
|
||||
# The bracketed-hash style is what the renderer uses for the spool ID;
|
||||
|
||||
Reference in New Issue
Block a user