Add configurable grid + snap to the layout system
Two independent grids, each with a size and a snap toggle: - Garden grid (owner-set, in the garden settings form): drives the editor's visual grid and, when snapping is on, snaps objects on place/move and snaps their dimensions to whole grid steps on resize. - Bed grid (per plantable object, in the bed Inspector): when snapping is on, plants snap to the bed's grid on place/move, and the grid is drawn inside the focused bed. Plant radius stays free. Snapping defaults off on both, so existing gardens keep free placement. Grid spacing shares the [1cm, 100m] range and defaults to 1m (garden) / 30cm (bed); 0 is treated as unset and re-defaulted. Backend: migration 0005 adds grid_size_cm/snap_to_grid to gardens and garden_objects, threaded through domain/store/service/api with service + api round-trip tests. Frontend: new geometry snap helpers (unit-tested), schema/type plumbing, canvas + overlay snapping, and the two forms. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
@@ -48,6 +48,41 @@ func TestCreateObjectDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectGridSettings(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
g := seedGarden(t, s, owner)
|
||||
ctx := context.Background()
|
||||
|
||||
// An omitted grid size defaults; snap defaults off.
|
||||
o, err := s.CreateObject(ctx, owner, g.ID, ObjectInput{
|
||||
Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 200, HeightCM: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateObject: %v", err)
|
||||
}
|
||||
if o.GridSizeCM != defaultObjectGridCM {
|
||||
t.Errorf("grid = %v, want default %d", o.GridSizeCM, defaultObjectGridCM)
|
||||
}
|
||||
if o.SnapToGrid {
|
||||
t.Error("snap should default off")
|
||||
}
|
||||
|
||||
// Patching grid + snap round-trips and leaves other fields untouched.
|
||||
grid := 15.0
|
||||
snap := true
|
||||
up, err := s.UpdateObject(ctx, owner, o.ID, ObjectPatch{GridSizeCM: &grid, SnapToGrid: &snap}, o.Version)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateObject: %v", err)
|
||||
}
|
||||
if up.GridSizeCM != 15 || !up.SnapToGrid {
|
||||
t.Errorf("after patch grid=%v snap=%v, want 15/true", up.GridSizeCM, up.SnapToGrid)
|
||||
}
|
||||
if up.WidthCM != 200 || up.HeightCM != 100 {
|
||||
t.Errorf("a grid patch changed dimensions: %vx%v", up.WidthCM, up.HeightCM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateObjectPlantableByKind(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
@@ -105,6 +140,7 @@ func TestCreateObjectValidation(t *testing.T) {
|
||||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, Color: strPtr("nope")}, // bad color
|
||||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, Props: strPtr("{not json")}, // bad props
|
||||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, Name: strings.Repeat("x", maxObjectNameLen+1)}, // name too long
|
||||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, GridSizeCM: maxGardenCM + 1}, // grid over the cap
|
||||
}
|
||||
for i, in := range bad {
|
||||
if _, err := s.CreateObject(context.Background(), owner, g.ID, in); !errors.Is(err, domain.ErrInvalidInput) {
|
||||
|
||||
Reference in New Issue
Block a user