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:
+32
-15
@@ -13,11 +13,12 @@ import (
|
||||
// sanity range that also rejects NaN/Inf and absurd values. Name/notes are
|
||||
// length-capped so untrusted input can't balloon storage.
|
||||
const (
|
||||
defaultGardenCM = 1000 // 10 m
|
||||
minGardenCM = 1 // 1 cm
|
||||
maxGardenCM = 10_000 // 100 m
|
||||
maxGardenNameLen = 200
|
||||
maxGardenNotesLen = 10_000
|
||||
defaultGardenCM = 1000 // 10 m
|
||||
minGardenCM = 1 // 1 cm
|
||||
maxGardenCM = 10_000 // 100 m
|
||||
defaultGardenGridCM = 100 // 1 m, matching the editor's previous fixed grid
|
||||
maxGardenNameLen = 200
|
||||
maxGardenNotesLen = 10_000
|
||||
)
|
||||
|
||||
// gardenRole ranks a user's access to a garden. Higher includes lower
|
||||
@@ -48,11 +49,13 @@ func (r gardenRole) String() string {
|
||||
|
||||
// GardenInput is the mutable field set for creating or updating a garden.
|
||||
type GardenInput struct {
|
||||
Name string
|
||||
WidthCM float64
|
||||
HeightCM float64
|
||||
UnitPref string
|
||||
Notes string
|
||||
Name string
|
||||
WidthCM float64
|
||||
HeightCM float64
|
||||
UnitPref string
|
||||
Notes string
|
||||
GridSizeCM float64
|
||||
SnapToGrid bool
|
||||
}
|
||||
|
||||
// requireGardenRole loads a garden and enforces that the actor holds at least
|
||||
@@ -198,12 +201,26 @@ func gardenFromInput(in GardenInput, applyDefaults bool) (*domain.Garden, error)
|
||||
return nil, domain.ErrInvalidInput
|
||||
}
|
||||
|
||||
// Grid spacing shares the garden dimension range [1 cm, 100 m]. 0 means
|
||||
// "unset" — defaulted (not just on create: an older client that omits it must
|
||||
// not wipe the column to an invalid 0), so a saved garden always has a usable
|
||||
// grid even if snapping is off.
|
||||
grid := in.GridSizeCM
|
||||
if grid == 0 {
|
||||
grid = defaultGardenGridCM
|
||||
}
|
||||
if !validDimensionCM(grid) {
|
||||
return nil, domain.ErrInvalidInput
|
||||
}
|
||||
|
||||
return &domain.Garden{
|
||||
Name: name,
|
||||
WidthCM: width,
|
||||
HeightCM: height,
|
||||
UnitPref: unit,
|
||||
Notes: notes,
|
||||
Name: name,
|
||||
WidthCM: width,
|
||||
HeightCM: height,
|
||||
UnitPref: unit,
|
||||
Notes: notes,
|
||||
GridSizeCM: grid,
|
||||
SnapToGrid: in.SnapToGrid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user