From c5d2e323554e86a362c50e58357f3d51172bbd27 Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 26 May 2026 16:26:31 +0200 Subject: [PATCH] Added Configuration from Item and Plate --- internal/layout/layout.go | 75 ++++++ internal/layout/layout_test.go | 46 ++++ internal/model/configuration.go | 40 +++ internal/paths/paths.go | 7 +- internal/server/admin/static/index.html | 321 +++++++++++++++++++++++- internal/server/api/api.go | 2 + internal/server/api/configuration.go | 218 ++++++++++++++++ internal/store/configuration.go | 65 +++++ 8 files changed, 766 insertions(+), 8 deletions(-) create mode 100644 internal/layout/layout.go create mode 100644 internal/layout/layout_test.go create mode 100644 internal/model/configuration.go create mode 100644 internal/server/api/configuration.go create mode 100644 internal/store/configuration.go diff --git a/internal/layout/layout.go b/internal/layout/layout.go new file mode 100644 index 0000000..962f479 --- /dev/null +++ b/internal/layout/layout.go @@ -0,0 +1,75 @@ +package layout + +import ( + "printer.backend/internal/model" +) + +// Footprint returns the physical width and height one item occupies on the plate (mm). +// Includes bleed on all sides and the item's margin as a safe zone. +func Footprint(spec model.ItemSpec) (width, height float64) { + w := spec.SizeMM + 2*spec.BleedMM + 2*spec.MarginMM + return w, w +} + +// CellPitch returns width and height of one grid cell (footprint + spacing between items). +func CellPitch(spec model.ItemSpec, spacingMM float64) (width, height float64) { + fw, fh := Footprint(spec) + return fw + spacingMM, fh + spacingMM +} + +// Pack computes the maximum grid of items that fit on the plate. +func Pack(plate model.Plate, spec model.ItemSpec, spacingMM float64) model.LayoutPreview { + pw := plate.PrintableWidth() + ph := plate.PrintableHeight() + fw, fh := Footprint(spec) + cw, ch := CellPitch(spec, spacingMM) + + cols, rows := gridCount(pw, ph, cw, ch) + count := cols * rows + + ox := plate.MarginLeft + oy := plate.MarginTop + + positions := make([]model.LayoutPosition, 0, count) + for row := 0; row < rows; row++ { + for col := 0; col < cols; col++ { + positions = append(positions, model.LayoutPosition{ + XMM: ox + float64(col)*cw, + YMM: oy + float64(row)*ch, + }) + } + } + + return model.LayoutPreview{ + PlateWidthMM: plate.WidthMM, + PlateHeightMM: plate.HeightMM, + PrintableXMM: ox, + PrintableYMM: oy, + PrintableWMM: pw, + PrintableHMM: ph, + CellWidthMM: cw, + CellHeightMM: ch, + FootprintWMM: fw, + FootprintHMM: fh, + SpacingMM: spacingMM, + Columns: cols, + Rows: rows, + Count: count, + Positions: positions, + } +} + +func gridCount(printableW, printableH, cellW, cellH float64) (cols, rows int) { + if printableW <= 0 || printableH <= 0 || cellW <= 0 || cellH <= 0 { + return 0, 0 + } + cols = int(printableW / cellW) + rows = int(printableH / cellH) + if cols < 0 { + cols = 0 + } + if rows < 0 { + rows = 0 + } + return cols, rows +} diff --git a/internal/layout/layout_test.go b/internal/layout/layout_test.go new file mode 100644 index 0000000..7011b40 --- /dev/null +++ b/internal/layout/layout_test.go @@ -0,0 +1,46 @@ +package layout + +import ( + "testing" + + "printer.backend/internal/model" +) + +func TestPack(t *testing.T) { + plate := model.Plate{ + WidthMM: 300, + HeightMM: 400, + MarginTop: 10, + MarginRight: 10, + MarginBottom: 10, + MarginLeft: 10, + } + spec := model.ItemSpec{SizeMM: 80, BleedMM: 2, MarginMM: 5} + // footprint = 80+4+10 = 94, cell = 94+2 = 96 + // printable 280x380 -> cols=2, rows=3 -> 6 + preview := Pack(plate, spec, 2) + if preview.Columns != 2 { + t.Fatalf("columns: got %d want 2", preview.Columns) + } + if preview.Rows != 3 { + t.Fatalf("rows: got %d want 3", preview.Rows) + } + if preview.Count != 6 { + t.Fatalf("count: got %d want 6", preview.Count) + } + if len(preview.Positions) != 6 { + t.Fatalf("positions: got %d want 6", len(preview.Positions)) + } + if preview.Positions[0].XMM != 10 || preview.Positions[0].YMM != 10 { + t.Fatalf("first position: got (%v,%v)", preview.Positions[0].XMM, preview.Positions[0].YMM) + } +} + +func TestPackZeroCell(t *testing.T) { + plate := model.Plate{WidthMM: 10, HeightMM: 10} + spec := model.ItemSpec{SizeMM: 100} + preview := Pack(plate, spec, 0) + if preview.Count != 0 { + t.Fatalf("expected 0 items, got %d", preview.Count) + } +} diff --git a/internal/model/configuration.go b/internal/model/configuration.go new file mode 100644 index 0000000..e341bda --- /dev/null +++ b/internal/model/configuration.go @@ -0,0 +1,40 @@ +package model + +import "time" + +// Configuration links a plate and item with layout spacing for maximum packing. +type Configuration struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + PlateID string `json:"plate_id"` + ItemID string `json:"item_id"` + SpacingMM float64 `json:"spacing_mm"` + CreatedAt time.Time `json:"created_at"` +} + +// LayoutPosition is the top-left corner of one item on the plate (mm). +type LayoutPosition struct { + XMM float64 `json:"x_mm"` + YMM float64 `json:"y_mm"` +} + +// LayoutPreview describes how many items fit on a plate. +type LayoutPreview struct { + PlateID string `json:"plate_id"` + ItemID string `json:"item_id"` + SpacingMM float64 `json:"spacing_mm"` + PlateWidthMM float64 `json:"plate_width_mm"` + PlateHeightMM float64 `json:"plate_height_mm"` + PrintableXMM float64 `json:"printable_x_mm"` + PrintableYMM float64 `json:"printable_y_mm"` + PrintableWMM float64 `json:"printable_width_mm"` + PrintableHMM float64 `json:"printable_height_mm"` + CellWidthMM float64 `json:"cell_width_mm"` + CellHeightMM float64 `json:"cell_height_mm"` + FootprintWMM float64 `json:"footprint_width_mm"` + FootprintHMM float64 `json:"footprint_height_mm"` + Columns int `json:"columns"` + Rows int `json:"rows"` + Count int `json:"count"` + Positions []LayoutPosition `json:"positions"` +} diff --git a/internal/paths/paths.go b/internal/paths/paths.go index 539988b..8fb0007 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -2,7 +2,8 @@ package paths // Runtime data directories (relative to process working directory). const ( - DataDir = "data" - PlatesDir = "data/plates" - SVGTemplateDir = "data/svg_template" + DataDir = "data" + PlatesDir = "data/plates" + SVGTemplateDir = "data/svg_template" + ConfigurationsDir = "data/configurations" ) diff --git a/internal/server/admin/static/index.html b/internal/server/admin/static/index.html index 20ce2b3..a64bb58 100644 --- a/internal/server/admin/static/index.html +++ b/internal/server/admin/static/index.html @@ -151,6 +151,63 @@ padding: 0 0.75rem 0.75rem; } .row-actions { display: flex; gap: 0.5rem; justify-content: flex-end; } + .layout-preview-wrap { + margin-top: 1rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + overflow: auto; + } + .layout-preview-wrap svg { + display: block; + max-width: 100%; + height: auto; + margin: 0 auto; + } + .layout-stats { + display: flex; + flex-wrap: wrap; + gap: 1rem 2rem; + margin-top: 0.75rem; + font-size: 0.85rem; + } + .layout-stats strong { color: var(--text); } + .layout-stats span { color: var(--muted); } + select { + width: 100%; + margin-top: 0.5rem; + padding: 0.5rem 0.75rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + font-size: 0.875rem; + } + .config-list { + display: grid; + gap: 1rem; + margin-top: 1rem; + } + .config-entry { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + } + .config-entry header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 0; + padding-bottom: 0.5rem; + border-bottom: none; + } + .config-entry header h3 { + font-size: 0.95rem; + font-weight: 600; + } @@ -275,6 +332,88 @@ +
+

Konfiguration — Layout-Vorschau

+

Kombiniert Platte und Item; berechnet maximale Stückzahl unter Einhaltung aller Margins und Abstände.

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +

+

+
+ + +

+ Keine Vorschau (Platte zu klein oder API-Fehler). +

+

Berechne Layout…

+
+ +
+

Gespeicherte Konfigurationen

+
+ +
+
+

Items