Plops editor: focus mode, place/move/resize, semantic zoom (#15)
Build image / build-and-push (push) Successful in 14s
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:
@@ -0,0 +1,93 @@
|
||||
import { memo, useMemo } from 'react'
|
||||
import type { Plant } from '@/lib/plants'
|
||||
import type { EditorPlanting } from '@/lib/plantings'
|
||||
import { PlopMarker, SEMANTIC_FAR } from './PlopMarker'
|
||||
import { DIMMED_OPACITY, objectTransform } from './shared'
|
||||
import type { EditorObject } from './types'
|
||||
|
||||
/** The most common plant color among an object's plops (for the far-zoom tint). */
|
||||
function dominantColor(plops: EditorPlanting[], plantsById: Map<number, Plant>): string | null {
|
||||
const freq = new Map<string, number>()
|
||||
for (const p of plops) {
|
||||
const c = plantsById.get(p.plantId)?.color
|
||||
if (c) freq.set(c, (freq.get(c) ?? 0) + 1)
|
||||
}
|
||||
let best: string | null = null
|
||||
let bestN = 0
|
||||
for (const [c, n] of freq) {
|
||||
if (n > bestN) {
|
||||
best = c
|
||||
bestN = n
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders every active plop, grouped under its parent object's translate+rotate
|
||||
* transform so plops track the bed when it's moved/rotated. Far zoom adds a
|
||||
* dominant-plant tint over each planted object so beds read at a glance. Focus
|
||||
* mode dims plops on non-focused objects.
|
||||
*/
|
||||
export const PlopLayer = memo(function PlopLayer({
|
||||
objects,
|
||||
plantings,
|
||||
plantsById,
|
||||
scale,
|
||||
focusedObjectId,
|
||||
selectedPlantingId,
|
||||
onSelectPlop,
|
||||
}: {
|
||||
objects: EditorObject[]
|
||||
plantings: EditorPlanting[]
|
||||
plantsById: Map<number, Plant>
|
||||
scale: number
|
||||
focusedObjectId: number | null
|
||||
selectedPlantingId: number | null
|
||||
onSelectPlop: (id: number) => void
|
||||
}) {
|
||||
// Group plops by object once per plops change (not on every pan/zoom frame).
|
||||
const byObject = useMemo(() => {
|
||||
const m = new Map<number, EditorPlanting[]>()
|
||||
for (const p of plantings) {
|
||||
const arr = m.get(p.objectId)
|
||||
if (arr) arr.push(p)
|
||||
else m.set(p.objectId, [p])
|
||||
}
|
||||
return m
|
||||
}, [plantings])
|
||||
const far = scale < SEMANTIC_FAR
|
||||
|
||||
return (
|
||||
<>
|
||||
{objects.map((o) => {
|
||||
const plops = byObject.get(o.id)
|
||||
if (!plops || plops.length === 0) return null
|
||||
const dimmed = focusedObjectId != null && focusedObjectId !== o.id
|
||||
const halfW = o.widthCm / 2
|
||||
const halfH = o.heightCm / 2
|
||||
const tint = far ? dominantColor(plops, plantsById) : null
|
||||
return (
|
||||
<g key={o.id} transform={objectTransform(o)} opacity={dimmed ? DIMMED_OPACITY : 1}>
|
||||
{tint &&
|
||||
(o.shape === 'circle' ? (
|
||||
<ellipse cx={0} cy={0} rx={halfW} ry={halfH} fill={tint} fillOpacity={0.5} pointerEvents="none" />
|
||||
) : (
|
||||
<rect x={-halfW} y={-halfH} width={halfW * 2} height={halfH * 2} fill={tint} fillOpacity={0.5} pointerEvents="none" />
|
||||
))}
|
||||
{plops.map((p) => (
|
||||
<PlopMarker
|
||||
key={p.id}
|
||||
plop={p}
|
||||
plant={plantsById.get(p.plantId)}
|
||||
scale={scale}
|
||||
selected={p.id === selectedPlantingId}
|
||||
onSelect={onSelectPlop}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user