// Plantings ("plops") data layer: the zod shape for a /full planting plus the // EditorPlanting the canvas renders. Optimistic create/update/remove mutations // live in objects.ts alongside the FullGarden cache they patch (a plop is part // of the one-shot editor payload). import { z } from 'zod' export const serverPlantingSchema = z.object({ id: z.number(), objectId: z.number(), plantId: z.number(), xCm: z.number(), yCm: z.number(), radiusCm: z.number(), count: z.number().nullable().optional(), // null/absent = derived label: z.string().nullable().optional(), plantedAt: z.string().nullable().optional(), removedAt: z.string().nullable().optional(), derivedCount: z.number(), // computed server-side from area / spacing² version: z.number(), createdAt: z.string(), updatedAt: z.string(), }) export type ServerPlanting = z.infer /** The plop shape the canvas renders. x/y/radius are in the parent object's * local frame (origin at object center), so a plop tracks its object. */ export interface EditorPlanting { id: number objectId: number plantId: number xCm: number yCm: number radiusCm: number count: number | null derivedCount: number label: string | null plantedAt: string | null version: number } export function toEditorPlanting(p: ServerPlanting): EditorPlanting { return { id: p.id, objectId: p.objectId, plantId: p.plantId, xCm: p.xCm, yCm: p.yCm, radiusCm: p.radiusCm, count: p.count ?? null, derivedCount: p.derivedCount, label: p.label ?? null, plantedAt: p.plantedAt ?? null, version: p.version, } } /** The count a plop shows: its explicit override, else the derived value. */ export function effectiveCount(p: { count: number | null; derivedCount: number }): number { return p.count ?? p.derivedCount } /** * Plant ids a garden has been planted with, most recent first and de-duplicated — * the "what have I actually been planting here" quick list (#100). Ordered by * plantedAt (a plop with no date sorts last), then by id so newer plops win a * same-day tie. The caller resolves the ids to plants against the catalog. */ export function recentlyPlantedIds(plantings: EditorPlanting[]): number[] { const sorted = [...plantings].sort( (a, b) => (b.plantedAt ?? '').localeCompare(a.plantedAt ?? '') || b.id - a.id, ) const seen = new Set() const ids: number[] = [] for (const p of sorted) { if (!seen.has(p.plantId)) { seen.add(p.plantId) ids.push(p.plantId) } } return ids } /** Client-side mirror of the server's derived-count formula, for live display * while resizing a plop (before the PATCH round-trips). max(1, round(π·r² / * spacing²)), capped like the server. */ export function computeDerivedCount(radiusCm: number, spacingCm: number): number { if (radiusCm <= 0 || spacingCm <= 0) return 1 const n = Math.round((Math.PI * radiusCm * radiusCm) / (spacingCm * spacingCm)) return Math.max(1, Math.min(1_000_000, n)) }