From 3af0d087790a524c6adbc8d2dad57ae26bb93a72 Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Tue, 21 Jul 2026 10:13:33 -0400 Subject: [PATCH] Fill: plant nothing for a region that misses the object entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clampTo INVERTS a region lying wholly outside the object — Max clamps below Min — rather than emptying it. The old loop-until-past-MaxX form handled that for free by never entering the loop. Counting positions up front does not: a region 500cm east of a bed with ±50cm local bounds produced 4 plops at x=275, a couple of metres off the bed. Caught by removing the guard and watching the new test fail, not by assuming it would. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ --- internal/service/ops.go | 7 +++++++ internal/service/ops_test.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/internal/service/ops.go b/internal/service/ops.go index 5ab9ff6..a0c9865 100644 --- a/internal/service/ops.go +++ b/internal/service/ops.go @@ -195,6 +195,13 @@ func hexCenters(r Region, radius, spacing float64) []localPoint { if radius <= 0 { return nil } + // clampTo INVERTS a region that lies wholly outside the object (Max clamps + // below Min), and an inverted region has no inside to plant. The old + // loop-until-past-MaxX form got this for free by never entering the loop; + // counting positions up front does not, and would site a plop off the bed. + if r.MaxX < r.MinX || r.MaxY < r.MinY { + return nil + } pitch := 2 * radius rowH := pitch * math.Sqrt(3) / 2 diff --git a/internal/service/ops_test.go b/internal/service/ops_test.go index b2e5d11..d36e499 100644 --- a/internal/service/ops_test.go +++ b/internal/service/ops_test.go @@ -161,6 +161,27 @@ func TestHexCentersTinyRegion(t *testing.T) { } } +// TestFillRegionOutsideObjectPlantsNothing covers a region that misses the object +// entirely. clampTo inverts such a region rather than emptying it, and an +// inverted region must plant nothing — not one plop at some point off the bed. +func TestFillRegionOutsideObjectPlantsNothing(t *testing.T) { + ctx := context.Background() + s := newTestService(t, openConfig()) + owner := seedUser(t, s, "a@example.com") + g, _ := s.CreateGarden(ctx, owner, GardenInput{Name: "Big", WidthCM: 2000, HeightCM: 2000}) + bed := seedFillBed(t, s, owner, g.ID, 100, 100) // local bounds ±50 + plant := seedOwnPlant(t, s, owner, 10) + + // Wholly east of the bed: clampTo gives MinX=500, MaxX=50. + created, err := s.FillRegion(ctx, owner, bed.ID, rect(500, -50, 600, 50), plant.ID, nil) + if err != nil { + t.Fatalf("FillRegion: %v", err) + } + if len(created) != 0 { + t.Errorf("filled %d plops for a region outside the bed, want 0: %+v", len(created), created) + } +} + // seedFillBed makes a plantable bed of the given size centered in a big garden. func seedFillBed(t *testing.T, s *Service, owner, gardenID int64, w, h float64) *domain.GardenObject { t.Helper()