Field editor: palette, select/move/resize/rotate, inspector, optimistic sync (#11) (#30)
Build image / build-and-push (push) Successful in 13s
Build image / build-and-push (push) Successful in 13s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #30.
This commit is contained in:
@@ -1,31 +1,79 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { getRouteApi } from '@tanstack/react-router'
|
||||
import { Alert } from '@/components/ui/Alert'
|
||||
import { GardenCanvas } from '@/editor/GardenCanvas'
|
||||
import type { EditorGarden, EditorObject } from '@/editor/types'
|
||||
import { Inspector } from '@/editor/Inspector'
|
||||
import { Palette } from '@/editor/Palette'
|
||||
import { useEditorStore } from '@/editor/store'
|
||||
import type { EditorGarden } from '@/editor/types'
|
||||
import { toEditorObject, useGardenFull } from '@/lib/objects'
|
||||
|
||||
// Mock scene for the canvas foundation (#9). #11 replaces this with the garden's
|
||||
// real /full payload keyed on the route's gardenId.
|
||||
const mockGarden: EditorGarden = { id: 0, name: 'Demo garden', widthCm: 1200, heightCm: 800, unitPref: 'metric' }
|
||||
|
||||
const mockObjects: EditorObject[] = [
|
||||
{ id: 1, kind: 'bed', name: 'Bed A', shape: 'rect', xCm: 300, yCm: 250, widthCm: 400, heightCm: 120, rotationDeg: 0, zIndex: 1, plantable: true },
|
||||
{ id: 2, kind: 'bed', name: 'Bed B', shape: 'rect', xCm: 300, yCm: 450, widthCm: 400, heightCm: 120, rotationDeg: 0, zIndex: 1, plantable: true },
|
||||
{ id: 3, kind: 'container', name: 'Pot', shape: 'circle', xCm: 800, yCm: 250, widthCm: 120, heightCm: 120, rotationDeg: 0, zIndex: 2, plantable: true },
|
||||
{ id: 4, kind: 'path', name: '', shape: 'rect', xCm: 600, yCm: 400, widthCm: 60, heightCm: 720, rotationDeg: 0, zIndex: 0, plantable: false },
|
||||
{ id: 5, kind: 'tree', name: 'Apple', shape: 'circle', xCm: 980, yCm: 620, widthCm: 200, heightCm: 200, rotationDeg: 0, zIndex: 2, plantable: false },
|
||||
{ id: 6, kind: 'bed', name: 'Angled', shape: 'rect', xCm: 920, yCm: 480, widthCm: 260, heightCm: 100, rotationDeg: 30, zIndex: 1, plantable: true },
|
||||
]
|
||||
const routeApi = getRouteApi('/gardens/$gardenId')
|
||||
|
||||
export function GardenEditorPage() {
|
||||
const { gardenId } = routeApi.useParams()
|
||||
const gid = Number(gardenId)
|
||||
const { focus } = routeApi.useSearch()
|
||||
const full = useGardenFull(gid)
|
||||
|
||||
const selectedId = useEditorStore((s) => s.selectedId)
|
||||
const select = useEditorStore((s) => s.select)
|
||||
const setArmedKind = useEditorStore((s) => s.setArmedKind)
|
||||
const setLiveObject = useEditorStore((s) => s.setLiveObject)
|
||||
|
||||
// Map the server rows to EditorObjects once per cache change. Rebuilding this
|
||||
// every render (e.g. on each viewport/selection update) would hand ObjectShape
|
||||
// fresh identities and defeat its memo, re-rendering every object.
|
||||
const serverObjects = full.data?.objects
|
||||
const objects = useMemo(() => serverObjects?.map(toEditorObject) ?? [], [serverObjects])
|
||||
|
||||
// Clear transient editor state on entering/switching gardens and on leaving.
|
||||
useEffect(() => {
|
||||
const reset = () => {
|
||||
select(null)
|
||||
setArmedKind(null)
|
||||
setLiveObject(null)
|
||||
}
|
||||
reset()
|
||||
return reset
|
||||
}, [gid, select, setArmedKind, setLiveObject])
|
||||
|
||||
if (full.isPending) return <p className="p-6 text-sm text-muted">Loading garden…</p>
|
||||
if (full.isError)
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Alert>Could not load this garden.</Alert>
|
||||
</div>
|
||||
)
|
||||
|
||||
const g = full.data.garden
|
||||
const garden: EditorGarden = {
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
widthCm: g.widthCm,
|
||||
heightCm: g.heightCm,
|
||||
unitPref: g.unitPref,
|
||||
}
|
||||
const selected = objects.find((o) => o.id === selectedId) ?? null
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-10rem)] flex-col gap-3">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<h1 className="text-lg font-semibold tracking-tight">Editor preview</h1>
|
||||
<span className="text-sm text-muted">
|
||||
Mock garden — drag to pan, wheel/pinch to zoom, tap an object to select. Real data + editing land in #11.
|
||||
</span>
|
||||
<div className="flex h-[calc(100vh-8rem)] flex-col gap-3 md:flex-row">
|
||||
<div className="shrink-0 md:w-40">
|
||||
<h1 className="mb-2 truncate text-lg font-semibold tracking-tight" title={garden.name}>
|
||||
{garden.name}
|
||||
</h1>
|
||||
<Palette />
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
<GardenCanvas garden={mockGarden} objects={mockObjects} />
|
||||
|
||||
<div className="relative min-h-0 flex-1">
|
||||
<GardenCanvas garden={garden} objects={objects} focusId={focus} />
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div className="fixed inset-x-0 bottom-0 z-30 max-h-[65vh] overflow-y-auto rounded-t-xl border-t border-border bg-surface p-4 shadow-lg md:static md:max-h-none md:w-72 md:shrink-0 md:rounded-xl md:border md:p-4 md:shadow-sm">
|
||||
<Inspector key={selected.id} object={selected} gardenId={gid} unit={garden.unitPref} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user