Merge pull request 'Fill mode: a "grid" layout that plants individual plants in rows' (#95) from feat/fill-mode into main
Build image / build-and-push (push) Successful in 6s

This commit was merged in pull request #95.
This commit is contained in:
2026-07-22 04:22:46 +00:00
9 changed files with 239 additions and 66 deletions
+6 -2
View File
@@ -51,6 +51,10 @@ type objectFillRequest struct {
// SpacingOverrideCM plants tighter or looser than the plant's mature spacing
// without editing the catalog entry.
SpacingOverrideCM *float64 `json:"spacingOverrideCm"`
// Layout is "clump" (default; fat clumps for a quick sketch) or "grid"
// (individual plants in rows at true spacing). Empty = clump. An unknown value
// is refused by the service (#77).
Layout string `json:"layout"`
}
func (h *handlers) fillObject(c *gin.Context) {
@@ -84,9 +88,9 @@ func (h *handlers) fillObject(c *gin.Context) {
return
}
region := service.Region{MinX: rect.MinX, MinY: rect.MinY, MaxX: rect.MaxX, MaxY: rect.MaxY}
created, err = h.svc.FillRegion(c.Request.Context(), actor, id, region, req.PlantID, req.SpacingOverrideCM)
created, err = h.svc.FillRegion(c.Request.Context(), actor, id, region, req.PlantID, req.SpacingOverrideCM, service.FillLayout(req.Layout))
} else {
created, err = h.svc.FillNamedRegion(c.Request.Context(), actor, id, req.Region, req.PlantID, req.SpacingOverrideCM)
created, err = h.svc.FillNamedRegion(c.Request.Context(), actor, id, req.Region, req.PlantID, req.SpacingOverrideCM, service.FillLayout(req.Layout))
}
if err != nil {
writeServiceError(c, err)
+38
View File
@@ -97,6 +97,44 @@ func TestFillAndClearAPI(t *testing.T) {
}
}
// TestFillLayoutAPI covers the layout selector (#77): grid packs denser than the
// clump default, and an unknown layout is a 400 rather than a silent clump fill.
func TestFillLayoutAPI(t *testing.T) {
r := authEngine(t, localCfg())
cookie := registerAndCookie(t, r, "[email protected]")
_, objID, plantID := seedFillableBed(t, r, cookie, 200, 200, 20)
// Clump (default) for the baseline count.
w := doJSON(t, r, http.MethodPost, fillPath(objID), map[string]any{
"plantId": plantID, "region": "all",
}, cookie)
if w.Code != http.StatusOK {
t.Fatalf("clump fill: status %d, body %s", w.Code, w.Body.String())
}
clumpN := int(decodeMap(t, w.Body.Bytes())["created"].(float64))
if w := doJSON(t, r, http.MethodPost, clearPath(objID), nil, cookie); w.Code != http.StatusOK {
t.Fatalf("clear between fills: %d", w.Code)
}
// Grid packs individual plants at true spacing — many more, small plops.
w = doJSON(t, r, http.MethodPost, fillPath(objID), map[string]any{
"plantId": plantID, "region": "all", "layout": "grid",
}, cookie)
if w.Code != http.StatusOK {
t.Fatalf("grid fill: status %d, body %s", w.Code, w.Body.String())
}
if gridN := int(decodeMap(t, w.Body.Bytes())["created"].(float64)); gridN <= clumpN {
t.Errorf("grid fill created %d, want more than the clump fill's %d", gridN, clumpN)
}
// An unknown layout is a 400, not a silent clump fill.
if w := doJSON(t, r, http.MethodPost, fillPath(objID), map[string]any{
"plantId": plantID, "region": "all", "layout": "spiral",
}, cookie); w.Code != http.StatusBadRequest {
t.Errorf("unknown layout = %d, want 400", w.Code)
}
}
// TestFillRegionSelectionAPI: exactly one of region/rect, and a rect fills only
// its own corner of the bed.
func TestFillRegionSelectionAPI(t *testing.T) {