feat: drop new nodes at centre of visible canvas

New nodes previously landed at a fixed canvas origin, often off-screen,
forcing the user to drag them into view. Add a projector (registered by
CanvasContainer inside ReactFlowProvider) that maps the visible-canvas
centre into flow coordinates, and use it for add-node, group rect, text,
Zigbee/Z-Wave imports, and pending-device approval (single + bulk).

ha-relevant: yes
This commit is contained in:
Pouzor
2026-06-28 00:27:18 +02:00
parent e811d83ceb
commit 2a8c9d618b
5 changed files with 101 additions and 9 deletions
@@ -0,0 +1,36 @@
import { afterEach, describe, expect, it } from 'vitest'
import {
getCenteredPosition,
getViewportCenter,
setViewportCenterProjector,
} from '../viewportCenter'
afterEach(() => setViewportCenterProjector(null))
describe('viewportCenter', () => {
it('falls back to a fixed point when no projector is registered', () => {
expect(getViewportCenter()).toEqual({ x: 300, y: 300 })
expect(getCenteredPosition()).toEqual({ x: 300, y: 300 })
})
it('returns the registered projector value as the centre', () => {
setViewportCenterProjector(() => ({ x: 1000, y: 500 }))
expect(getViewportCenter()).toEqual({ x: 1000, y: 500 })
})
it('offsets by half the box size so the box is centred', () => {
setViewportCenterProjector(() => ({ x: 1000, y: 500 }))
expect(getCenteredPosition(360, 240)).toEqual({ x: 820, y: 380 })
})
it('treats a zero size as the raw centre point', () => {
setViewportCenterProjector(() => ({ x: 42, y: 7 }))
expect(getCenteredPosition(0, 0)).toEqual({ x: 42, y: 7 })
})
it('clears the projector when set to null', () => {
setViewportCenterProjector(() => ({ x: 1, y: 2 }))
setViewportCenterProjector(null)
expect(getViewportCenter()).toEqual({ x: 300, y: 300 })
})
})
+30
View File
@@ -0,0 +1,30 @@
import type { XYPosition } from '@xyflow/react'
// Projects the centre of the visible canvas into flow-space coordinates.
// Registered by CanvasContainer (inside ReactFlowProvider) so that add-node
// handlers living outside the provider — App handlers, modals — can still drop
// new nodes where the user is actually looking instead of at a fixed canvas
// origin.
type Projector = () => XYPosition
let projector: Projector | null = null
// Used before any canvas is mounted (tests, first render). Matches the old
// hard-coded add position so behaviour degrades gracefully.
const FALLBACK: XYPosition = { x: 300, y: 300 }
export function setViewportCenterProjector(fn: Projector | null): void {
projector = fn
}
// Flow-space coordinate at the centre of the visible canvas.
export function getViewportCenter(): XYPosition {
return projector ? projector() : { ...FALLBACK }
}
// Flow-space top-left position so that a box of `width`×`height` ends up
// centred on screen. With no size it returns the raw centre point.
export function getCenteredPosition(width = 0, height = 0): XYPosition {
const c = getViewportCenter()
return { x: c.x - width / 2, y: c.y - height / 2 }
}