Plops editor: focus mode, place/move/resize, semantic zoom (#15)
Build image / build-and-push (push) Successful in 14s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #34.
This commit is contained in:
2026-07-19 03:22:51 +00:00
committed by steve
parent f4e5dab98c
commit 48ba08e8f2
14 changed files with 1084 additions and 73 deletions
+70
View File
@@ -0,0 +1,70 @@
// 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<typeof serverPlantingSchema>
/** 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))
}