// 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 } /** 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)) }