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,88 @@
|
||||
import { memo, type PointerEvent } from 'react'
|
||||
import type { Plant } from '@/lib/plants'
|
||||
import { computeDerivedCount, type EditorPlanting } from '@/lib/plantings'
|
||||
import { SELECT_COLOR } from './shared'
|
||||
|
||||
// Semantic-zoom thresholds in px/cm (DESIGN § Editor / rendering), tuned by feel:
|
||||
// < FAR — flat color patch, no text ("what's planted where" at a glance)
|
||||
// FAR..NEAR — colored circle + plant emoji
|
||||
// ≥ NEAR — circle + emoji + plant name + count
|
||||
export const SEMANTIC_FAR = 0.75
|
||||
export const SEMANTIC_NEAR = 3
|
||||
|
||||
const DEFAULT_COLOR = '#6aa84f'
|
||||
|
||||
/**
|
||||
* One plop, rendered in its parent object's local frame (the caller wraps it in
|
||||
* the object's translate+rotate group, so the plop tracks the bed). A circle in
|
||||
* the plant's color; emoji and name/count fade in with zoom. The count is
|
||||
* computed live from the current radius so it updates while resizing. memo'd so a
|
||||
* pan/zoom that only changes the world transform doesn't re-render every plop.
|
||||
*/
|
||||
export const PlopMarker = memo(function PlopMarker({
|
||||
plop,
|
||||
plant,
|
||||
scale,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
plop: EditorPlanting
|
||||
plant?: Plant
|
||||
scale: number
|
||||
selected: boolean
|
||||
onSelect: (id: number) => void
|
||||
}) {
|
||||
const color = plant?.color ?? DEFAULT_COLOR
|
||||
const r = Math.max(0, plop.radiusCm)
|
||||
const showIcon = scale >= SEMANTIC_FAR && !!plant?.icon
|
||||
const showText = scale >= SEMANTIC_NEAR && !!plant
|
||||
const count = plop.count ?? (plant ? computeDerivedCount(plop.radiusCm, plant.spacingCm) : plop.derivedCount)
|
||||
|
||||
function down(e: PointerEvent) {
|
||||
e.stopPropagation()
|
||||
onSelect(plop.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<g transform={`translate(${plop.xCm} ${plop.yCm})`} onPointerDown={down} style={{ cursor: 'pointer' }}>
|
||||
<circle
|
||||
cx={0}
|
||||
cy={0}
|
||||
r={r}
|
||||
fill={color}
|
||||
fillOpacity={0.82}
|
||||
stroke={selected ? SELECT_COLOR : '#00000033'}
|
||||
strokeWidth={selected ? 2.5 : 1}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
{showIcon && (
|
||||
<text
|
||||
x={0}
|
||||
y={0}
|
||||
fontSize={r * 1.2}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
||||
>
|
||||
{plant!.icon}
|
||||
</text>
|
||||
)}
|
||||
{showText && (
|
||||
<text
|
||||
x={0}
|
||||
y={r + r * 0.3}
|
||||
fontSize={Math.max(r * 0.5, 6)}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="hanging"
|
||||
fill="#1f2937"
|
||||
stroke="#ffffff"
|
||||
strokeWidth={0.5}
|
||||
paintOrder="stroke"
|
||||
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
||||
>
|
||||
{plant!.name} · {count}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user