# amoeba_grid — reference for AI assistants Flutter package. Dashboard grid of draggable, reshapeable polyomino cards with amoeba-style collision resolution and per-breakpoint persistence. Import: `package:amoeba_grid/amoeba_grid.dart`. Pure Flutter (no plugins). ## Coordinate & geometry model - The field is a grid of SQUARE units. `AmoebaGridConfig.columns/rows` are MINIMUMS: effective counts (`GridMetrics.columns/rows`) grow to fill the viewport at `maxCellExtent`, and further to cover any occupied cell. - Cell extent flexes: `clamp((viewportWidth - gap*(cols+1)) / cols, minCellExtent, maxCellExtent)`. Pitch = cellExtent + gap. When even minCellExtent doesn't fit, the field pans (TwoDimensionalScrollable). - `CellIndex(col, row)`: absolute grid address. `CardShape`: immutable Set; encodes position AND silhouette; must be non-empty; polyominoes allowed. `CardShape.rect(col, row, w, h)` helper. - Gaps: cards inset gap/2 inside their cell footprint. Gaps between a card's own cells are visually bridged; between different cards exactly one `gap` is always respected, including diagonal pinches. - Outlines: traced boundary of the cell-union, inset gap/2, rounded with `outsideCornerRadius` (convex) / `insideCornerRadius` (concave), clamped so arcs never overlap. Shape morphs animate by path resampling. ## Core API AmoebaGridConfig({columns=8, rows=12, minCellExtent=72, maxCellExtent=160, gap=12, insideCornerRadius=10, outsideCornerRadius=22, breakpoints}); breakpoints default [0,600,905,1240,1600] (viewport-width buckets). AmoebaGridController({required config, AmoebaGridStorage? storage, String? storageKey}) - load(): Future — reads persisted layout; call once (the view does it). - registerCards(Map initialShapes) — view does it. - effectiveShape(id) / committedShape(id): CardShape? - session: DragSession? (kind move|resize, cardId, preview, submissives) - resetLayout(): clears all persisted overrides. - occupiedColumns/occupiedRows: feed metrics growth. AmoebaGridView({required controller, required cards, AmoebaGridStyle? style}) AmoebaGridCard({required id, required initialShape, required child, color}) - User shaping persists and OVERRIDES initialShape (mobile-first per viewport-width bucket: largest breakpoint <= width wins). AmoebaGridStorage: two methods — Future read(key), Future write(key, value). Default: in-memory. Layout JSON is versioned; key default 'amoeba_grid.layout.v1'. AmoebaGridStyle.fromTheme(theme) or const AmoebaGridStyle(cardColor, cardBorderColor, accentColor, handleColor, backdropDotColor, cardElevation). ## Interaction semantics (what gestures do) - Body drag = MOVE. Preview (accent outline) snaps at 50% of each cell crossing (pitch covers cell+gap). Drop commits; Esc cancels. - Side handle drag = STRIP resize: only the row/col strip under that edge segment extends/retracts. Retraction never empties or disconnects a shape. L-gesture: after dragging outward, perpendicular movement grows ONLY the new section. - Corner handle drag (convex AND concave notches) = both full edge segments through the corner move; diagonal fill keeps convex corners square. Concave pulls fill/carve the notch. - Collisions (aggressor preview vs other cards' committed shapes): the submissive cedes cells from the edge the aggressor hit (decided edge-to-edge at contact; re-decided per fresh contact), per overlapped row/col through the aggressor's far edge. No remainder -> relocate opposite the entry edge (one cascade level). Transient: reverts the moment overlap ends; on drop every active submissive commits its shape. - Hit testing: containment-first (a press inside a card belongs to it); handles are whole-edge bands (interior reach 12px, outward ~hitRadius); in gutters the nearest handle wins (midline split); corners outrank sides; a hover-revealed handle owns the press anywhere in its zone. - Keyboard: Escape cancels an active drag. ## Shape-aware content widgets All read AmoebaCardScope (auto-injected around every card child); all degrade to plain rectangular behavior outside a card. Geometry (AmoebaCardGeometry): size, path, rowBands/columnBands (merged free spans), largestRect, regions (greedy maximal-rectangle decomposition), insets. - AmoebaContentArea({child, padding, alignment}): lays child in largestRect — never bitten by notches. - AmoebaRegions({builder(context, AmoebaRegion)->Widget?}): one call per rectangular sub-region, area-descending; region.isLargest, cellWidth/ cellHeight, rect. - AmoebaFlow({axis, spacing, alignment(start|center|end|stretch), children}) + AmoebaColumn/AmoebaRow presets: children constrained to the free span at their main-axis position; band-straddlers get the span intersection. - AmoebaText(text, {style, lineSpacing}): greedy band-by-band line layout; wraps around notches incl. split spans; no selection; single style. - AmoebaPadding({padding, child}): pads AND republishes deflated geometry. Outline-aware: interior card edges (notches/steps) receive padding too. Use it (not plain Padding) above any Amoeba* layout widget. - Constraint: AmoebaFlow/AmoebaText must fill their scope box; use AmoebaPadding to inset, never a bare Padding, or their band coordinates misalign. ## Diagnostics AmoebaGridDiagnostics.enabled = true (only honored in debug; release-inert). .events: broadcast Stream. Kinds: metricsResolved, handleHoverEnter/Exit, pointerDown (position + what it hit), gestureAccepted/Rejected (arena outcomes), dragStart, dragUpdate, previewChanged, submissiveTrimmed/Relocated/Reverted, dragCancelled, layoutCommitted (includes full committed layout on recompute), layoutLoaded/Saved, edgeAutoScroll. attachDebugPrintLogger() pipes to debugPrint with an [amoeba_grid] prefix. ## Recipes - Persist with shared_preferences: implement AmoebaGridStorage.read/write around SharedPreferences.getInstance(). - Different layouts per window size: automatic — edits save into the active viewport-width bucket and resolve mobile-first. - Programmatic reset: controller.resetLayout(). - Observe layout changes: listen to the controller (ChangeNotifier) or the diagnostics stream's layoutCommitted events. - Testing hit zones: engine functions handlesFor/hitTestHandles/ interactionAt in src/engine/handles.dart are pure and unit-testable; see the package's test/hitbox_test.dart for boundary-testing patterns. ## Gotchas - Card ids must be stable; shapes are keyed by id in persistence. - initialShape only applies where no persisted override exists for the active breakpoint chain. - Content in concave cutouts is clipped, not reflowed — use the content widgets above for reflow. - The morph animates the clip, not the layout: content lays out against the settled target shape (intentional; avoids per-frame reflow). - Trackpad two-finger pans always scroll the field; card drags require a button/touch drag.