package api import ( "net/http" "github.com/gin-gonic/gin" "gitea.stevedudenhoeffer.com/steve/pansy/internal/domain" "gitea.stevedudenhoeffer.com/steve/pansy/internal/service" ) // Bulk operations on a plantable object (#82): fill a region with one plant, and // clear everything out of it. // // These were reachable only through the agent toolbox until now, which meant the // most valuable bulk operation in a garden planner — and the one carrying the // most carefully reasoned geometry in the codebase — did not exist at all on an // instance with no model configured. They are thin adapters over the same // service methods `internal/agent/tools.go` calls, so the permission checks and // the one-change-set-per-operation guarantee come along unchanged. // fillRequest is the body for POST /objects/:id/fill. // // A region is given EITHER by compass name ("ne", "south half", "all") or as an // explicit rect in the object's local frame. The named form is what a person // means and what the agent uses; the rect is for a future drag-a-box affordance. // Exactly one must be supplied — accepting both and silently preferring one // would make a client bug look like a geometry bug. type fillRequest struct { PlantID int64 `json:"plantId" binding:"required"` Region string `json:"region"` Rect *struct { MinX float64 `json:"minXCm"` MinY float64 `json:"minYCm"` MaxX float64 `json:"maxXCm"` MaxY float64 `json:"maxYCm"` } `json:"rect"` // SpacingOverrideCM plants tighter or looser than the plant's mature spacing // without editing the catalog entry. SpacingOverrideCM *float64 `json:"spacingOverrideCm"` } func (h *handlers) fillObject(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } var req fillRequest if err := c.ShouldBindJSON(&req); err != nil { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "a plantId and a region are required") return } named, hasRect := req.Region != "", req.Rect != nil if named == hasRect { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", `supply exactly one of "region" (e.g. "all", "ne", "south half") or "rect"`) return } actor := mustActor(c).ID var ( created []domain.Planting err error ) if named { created, err = h.svc.FillNamedRegion(c.Request.Context(), actor, id, req.Region, req.PlantID, req.SpacingOverrideCM) } else { region := service.Region{MinX: req.Rect.MinX, MinY: req.Rect.MinY, MaxX: req.Rect.MaxX, MaxY: req.Rect.MaxY} created, err = h.svc.FillRegion(c.Request.Context(), actor, id, region, req.PlantID, req.SpacingOverrideCM) } if err != nil { writeServiceError(c, err) return } // 200, not 201: a fill can legitimately create nothing (the region is already // planted), and there is no single resource to point a Location at. c.JSON(http.StatusOK, gin.H{"plantings": created, "created": len(created)}) } // clearObject soft-removes every active plop in an object. // // Distinct from deleting the object, and — unlike the client-side loop this // replaces — it lands as ONE change set, so undoing a cleared bed is one click // rather than one per plop. func (h *handlers) clearObject(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } n, err := h.svc.ClearObject(c.Request.Context(), mustActor(c).ID, id) if err != nil { writeServiceError(c, err) return } c.JSON(http.StatusOK, gin.H{"cleared": n}) }