Merge pull request 'Canvas: make double-click-to-plant work on desktop' (#135) from fix/double-click-focus into main
Build image / build-and-push (push) Successful in 8s
Build image / build-and-push (push) Successful in 8s
This commit was merged in pull request #135.
This commit is contained in:
@@ -104,6 +104,12 @@ Conventions that follow from it:
|
|||||||
- **Tap-to-place makes a one-plant plop** (radius = spacing/2, per the handoff);
|
- **Tap-to-place makes a one-plant plop** (radius = spacing/2, per the handoff);
|
||||||
"Fill the bed" (rows / clumps) is the bulk tool. Fill geometry still follows
|
"Fill the bed" (rows / clumps) is the bulk tool. Fill geometry still follows
|
||||||
the clump rules below — different tools, not a conflict.
|
the clump rules below — different tools, not a conflict.
|
||||||
|
- **The canvas captures the pointer, so `onDoubleClick` on a bed never fires.**
|
||||||
|
`track()` calls `setPointerCapture` on the SVG root for every press, and
|
||||||
|
capture retargets the compatibility `click`/`dblclick` to the root. Double
|
||||||
|
presses are detected in `objDown` (same object, < 400 ms, < 12 px) instead;
|
||||||
|
a `dblclick` handler on an object is dead code — it was, silently, from the
|
||||||
|
Organic rebuild until 2026-08-23.
|
||||||
- **Undo in the header re-reads history before reverting.** The cached list
|
- **Undo in the header re-reads history before reverting.** The cached list
|
||||||
trails the canvas right after a placement, and undoing the step *before* the
|
trails the canvas right after a placement, and undoing the step *before* the
|
||||||
one you meant is the worst thing an undo button can do. Keep it that way.
|
one you meant is the worst thing an undo button can do. Keep it that way.
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ import {
|
|||||||
import { useEditorStore, type Viewport } from './store'
|
import { useEditorStore, type Viewport } from './store'
|
||||||
import type { EditorGarden, EditorObject } from './types'
|
import type { EditorGarden, EditorObject } from './types'
|
||||||
|
|
||||||
|
// A double press: same object, this close in time and space.
|
||||||
|
const DOUBLE_PRESS_MS = 400
|
||||||
|
const DOUBLE_PRESS_PX = 12
|
||||||
|
|
||||||
const WHEEL_SENSITIVITY = 0.0016
|
const WHEEL_SENSITIVITY = 0.0016
|
||||||
const ANIM_MS = 520
|
const ANIM_MS = 520
|
||||||
const REFIT_THRESHOLD_PX = 60
|
const REFIT_THRESHOLD_PX = 60
|
||||||
@@ -95,6 +99,13 @@ export const Canvas = forwardRef<
|
|||||||
const drag = useRef<Drag | null>(null)
|
const drag = useRef<Drag | null>(null)
|
||||||
const pinch = useRef<Pinch | null>(null)
|
const pinch = useRef<Pinch | null>(null)
|
||||||
const dropPlant = useRef<{ plant: Plant; lotId: number | null } | null>(null)
|
const dropPlant = useRef<{ plant: Plant; lotId: number | null } | null>(null)
|
||||||
|
// The last primary-button press on an object, for double-click detection.
|
||||||
|
// Detected in objDown rather than with onDoubleClick on the object's <g>:
|
||||||
|
// track() captures the pointer on the SVG root, and capture retargets the
|
||||||
|
// compatibility click and dblclick events to the root, so a dblclick
|
||||||
|
// handler on the object never fires — which is why "double-click a bed to
|
||||||
|
// plant it" did nothing from the Organic rebuild until 2026-08-23.
|
||||||
|
const lastPress = useRef<{ id: number; at: number; x: number; y: number } | null>(null)
|
||||||
|
|
||||||
const vp = useEditorStore((s) => s.vp)
|
const vp = useEditorStore((s) => s.vp)
|
||||||
const anim = useEditorStore((s) => s.anim)
|
const anim = useEditorStore((s) => s.anim)
|
||||||
@@ -316,6 +327,7 @@ export const Canvas = forwardRef<
|
|||||||
|
|
||||||
// ── pointer handlers ────────────────────────────────────────────────────
|
// ── pointer handlers ────────────────────────────────────────────────────
|
||||||
const onCanvasDown = (e: ReactPointerEvent) => {
|
const onCanvasDown = (e: ReactPointerEvent) => {
|
||||||
|
lastPress.current = null // a press on empty ground is not half of a double-click on a bed
|
||||||
track(e)
|
track(e)
|
||||||
if (pts.current.size === 2) return pinchStart()
|
if (pts.current.size === 2) return pinchStart()
|
||||||
const st = useEditorStore.getState()
|
const st = useEditorStore.getState()
|
||||||
@@ -342,6 +354,18 @@ export const Canvas = forwardRef<
|
|||||||
}
|
}
|
||||||
// Dimmed siblings stay inert inside a focused bed.
|
// Dimmed siblings stay inert inside a focused bed.
|
||||||
if (st.focusId != null && o.id !== st.focusId) return
|
if (st.focusId != null && o.id !== st.focusId) return
|
||||||
|
const prev = lastPress.current
|
||||||
|
const now = performance.now()
|
||||||
|
// Only the primary button (or a finger) counts, like a native dblclick.
|
||||||
|
lastPress.current = e.button === 0 && e.isPrimary ? { id: o.id, at: now, x: e.clientX, y: e.clientY } : null
|
||||||
|
if (prev && lastPress.current && prev.id === o.id && now - prev.at < DOUBLE_PRESS_MS && Math.hypot(e.clientX - prev.x, e.clientY - prev.y) < DOUBLE_PRESS_PX) {
|
||||||
|
// Second press of a double-click: focus the bed to plant it. No drag
|
||||||
|
// starts, so the matching pointerup has nothing to select into Plot.
|
||||||
|
lastPress.current = null
|
||||||
|
drag.current = null
|
||||||
|
if (o.plantable) focusObject(o)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!latest.current.canEdit) {
|
if (!latest.current.canEdit) {
|
||||||
st.setSel({ type: 'object', id: o.id })
|
st.setSel({ type: 'object', id: o.id })
|
||||||
st.setTab('plot')
|
st.setTab('plot')
|
||||||
@@ -562,7 +586,6 @@ export const Canvas = forwardRef<
|
|||||||
opacity={dim ? DIM_OBJECT : 1}
|
opacity={dim ? DIM_OBJECT : 1}
|
||||||
style={{ cursor: dim ? 'default' : canEdit ? 'grab' : 'pointer' }}
|
style={{ cursor: dim ? 'default' : canEdit ? 'grab' : 'pointer' }}
|
||||||
onPointerDown={objDown(o)}
|
onPointerDown={objDown(o)}
|
||||||
onDoubleClick={o.plantable && !dim ? () => focusObject(o) : undefined}
|
|
||||||
>
|
>
|
||||||
{o.shape === 'circle' ? (
|
{o.shape === 'circle' ? (
|
||||||
<ellipse rx={o.widthCm / 2} ry={o.heightCm / 2} {...common} />
|
<ellipse rx={o.widthCm / 2} ry={o.heightCm / 2} {...common} />
|
||||||
|
|||||||
Reference in New Issue
Block a user