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
+13 -6
View File
@@ -4,6 +4,7 @@ import { type Node } from '@xyflow/react'
import { applyDagreLayout } from '@/utils/layout'
import { serializeNode, serializeEdge, deserializeApiNode, deserializeApiEdge, type ApiNode, type ApiEdge } from '@/utils/canvasSerializer'
import { generateUUID } from '@/utils/uuid'
import { getCenteredPosition } from '@/utils/viewportCenter'
import { resolveVirtualEdgeParent } from '@/utils/virtualEdgeParent'
import { generateMarkdownTable } from '@/utils/exportMarkdown'
import { copyToClipboard } from '@/utils/clipboard'
@@ -259,7 +260,7 @@ export default function App() {
// extent, so we don't set them here.
const position = nestInParent && parentNode
? { x: parentNode.position.x + 20, y: parentNode.position.y + 50 }
: { x: 300, y: 300 }
: getCenteredPosition(isContainerNode ? 300 : 0, isContainerNode ? 200 : 0)
const newNode: Node<NodeData> = {
id,
@@ -278,7 +279,7 @@ export default function App() {
const newNode: Node<NodeData> = {
id,
type: 'groupRect',
position: { x: 200, y: 200 },
position: getCenteredPosition(360, 240),
data: {
label: data.label,
type: 'groupRect',
@@ -337,7 +338,7 @@ export default function App() {
// node fields; text_content is not in the schema and was lost on reload.
// TextNode and the edit modal both already fall back to label.
type: 'text',
position: { x: 250, y: 250 },
position: getCenteredPosition(200, 60),
data: {
label: data.text,
type: 'text',
@@ -502,15 +503,18 @@ export default function App() {
const handleZigbeeAddToCanvas = useCallback((zigbeeNodes: ZigbeeNode[], zigbeeEdges: ZigbeeEdge[]) => {
snapshotHistory()
// Place nodes in a grid starting at x=500, y=100
// Place nodes in a grid centred on the visible canvas.
const COLS = 4
const SPACING_X = 170
const SPACING_Y = 100
const cols = Math.min(COLS, zigbeeNodes.length)
const rows = Math.ceil(zigbeeNodes.length / COLS)
const origin = getCenteredPosition(cols * SPACING_X, rows * SPACING_Y)
zigbeeNodes.forEach((zn, i) => {
const id = zn.id
const col = i % COLS
const row = Math.floor(i / COLS)
const position = { x: 500 + col * SPACING_X, y: 100 + row * SPACING_Y }
const position = { x: origin.x + col * SPACING_X, y: origin.y + row * SPACING_Y }
const newNode: import('@xyflow/react').Node<NodeData> = {
id,
type: zn.type,
@@ -553,11 +557,14 @@ export default function App() {
const COLS = 4
const SPACING_X = 170
const SPACING_Y = 100
const cols = Math.min(COLS, zwaveNodes.length)
const rows = Math.ceil(zwaveNodes.length / COLS)
const origin = getCenteredPosition(cols * SPACING_X, rows * SPACING_Y)
zwaveNodes.forEach((zn, i) => {
const id = zn.id
const col = i % COLS
const row = Math.floor(i / COLS)
const position = { x: 500 + col * SPACING_X, y: 100 + row * SPACING_Y }
const position = { x: origin.x + col * SPACING_X, y: origin.y + row * SPACING_Y }
const newNode: import('@xyflow/react').Node<NodeData> = {
id,
type: zn.type,