Build image / build-and-push (push) Successful in 14s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
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>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
})
|