Center pasted Figma nodes in viewport instead of using original coordinates

This commit is contained in:
Danila Poyarkov 2026-03-02 17:32:06 +03:00
parent d7ba0bfef9
commit 8b1547ece7
5 changed files with 94 additions and 25 deletions

View file

@ -102,6 +102,50 @@ function decodeVectorData(nc: KiwiNodeChange, blobs: Uint8Array[]): VectorNetwor
}
}
const NON_VISUAL_TYPES = new Set([
'DOCUMENT', 'CANVAS', 'VARIABLE_SET', 'VARIABLE', 'VARIABLE_COLLECTION',
'STYLE', 'STYLE_SET', 'INTERNAL_ONLY_NODE', 'WIDGET', 'STAMP', 'STICKY',
'SHAPE_WITH_TEXT', 'CONNECTOR', 'CODE_BLOCK', 'TABLE_NODE', 'TABLE_CELL',
'SECTION_OVERLAY', 'SLIDE',
])
export function figmaNodesBounds(
nodeChanges: KiwiNodeChange[]
): { x: number; y: number; w: number; h: number } | null {
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
const parentTypes = new Map<string, string>()
for (const nc of nodeChanges) {
if (!nc.guid) continue
const id = `${nc.guid.sessionID}:${nc.guid.localID}`
parentTypes.set(id, nc.type ?? '')
}
for (const nc of nodeChanges) {
if (!nc.guid || !nc.type || NON_VISUAL_TYPES.has(nc.type)) continue
const parentId = nc.parentIndex?.guid
? `${nc.parentIndex.guid.sessionID}:${nc.parentIndex.guid.localID}`
: null
if (parentId && parentTypes.has(parentId) && !NON_VISUAL_TYPES.has(parentTypes.get(parentId)!))
continue
const x = nc.transform?.m02 ?? 0
const y = nc.transform?.m12 ?? 0
const w = nc.size?.x ?? 0
const h = nc.size?.y ?? 0
minX = Math.min(minX, x)
minY = Math.min(minY, y)
maxX = Math.max(maxX, x + w)
maxY = Math.max(maxY, y + h)
}
if (minX === Infinity) return null
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }
}
export function importClipboardNodes(
nodeChanges: KiwiNodeChange[],
graph: SceneGraph,
@ -121,31 +165,11 @@ export function importClipboardNodes(
}
}
const skipTypes = new Set([
'DOCUMENT',
'CANVAS',
'VARIABLE_SET',
'VARIABLE',
'VARIABLE_COLLECTION',
'STYLE',
'STYLE_SET',
'INTERNAL_ONLY_NODE',
'WIDGET',
'STAMP',
'STICKY',
'SHAPE_WITH_TEXT',
'CONNECTOR',
'CODE_BLOCK',
'TABLE_NODE',
'TABLE_CELL',
'SECTION_OVERLAY',
'SLIDE',
])
const topLevel: string[] = []
for (const [id, nc] of guidMap) {
if (skipTypes.has(nc.type ?? '')) continue
if (NON_VISUAL_TYPES.has(nc.type ?? '')) continue
const parentId = parentMap.get(id)
if (!parentId || !guidMap.has(parentId) || skipTypes.has(guidMap.get(parentId)?.type ?? '')) {
if (!parentId || !guidMap.has(parentId) || NON_VISUAL_TYPES.has(guidMap.get(parentId)?.type ?? '')) {
topLevel.push(id)
}
}
@ -243,7 +267,7 @@ export function importClipboardNodes(
const children: string[] = []
for (const [childId, pid] of parentMap) {
if (pid === figmaId && !skipTypes.has(guidMap.get(childId)?.type ?? '')) {
if (pid === figmaId && !NON_VISUAL_TYPES.has(guidMap.get(childId)?.type ?? '')) {
children.push(childId)
}
}

View file

@ -122,6 +122,7 @@ export {
export {
parseFigmaClipboard,
importClipboardNodes,
figmaNodesBounds,
parseOpenPencilClipboard,
buildFigmaClipboardHTML,
buildOpenPencilClipboardHTML,

View file

@ -1,6 +1,7 @@
export {
parseFigmaClipboard,
importClipboardNodes,
figmaNodesBounds,
parseOpenPencilClipboard,
buildFigmaClipboardHTML,
buildOpenPencilClipboardHTML,

View file

@ -12,6 +12,7 @@ import {
import {
parseFigmaClipboard,
importClipboardNodes,
figmaNodesBounds,
parseOpenPencilClipboard,
buildFigmaClipboardHTML,
buildOpenPencilClipboardHTML,
@ -1581,12 +1582,18 @@ export function createEditorStore() {
parseFigmaClipboard(html).then((figma) => {
if (figma) {
const bounds = figmaNodesBounds(figma.nodes)
const viewCenterX = (-state.panX + (window.innerWidth / 2)) / state.zoom
const viewCenterY = (-state.panY + (window.innerHeight / 2)) / state.zoom
const offsetX = bounds ? viewCenterX - (bounds.x + bounds.w / 2) : 0
const offsetY = bounds ? viewCenterY - (bounds.y + bounds.h / 2) : 0
const created = importClipboardNodes(
figma.nodes,
graph,
state.currentPageId,
20,
20,
offsetX,
offsetY,
figma.blobs
)
if (created.length > 0) {

View file

@ -3,6 +3,7 @@ import { describe, expect, it } from 'bun:test'
import {
parseFigmaClipboard,
importClipboardNodes,
figmaNodesBounds,
} from '../../packages/core/src/clipboard'
import { SceneGraph } from '../../packages/core/src/scene-graph'
@ -127,3 +128,38 @@ describe('importClipboardNodes', () => {
expect(node.strokes[0].weight).toBe(2)
})
})
describe('figmaNodesBounds', () => {
it('computes bounds of top-level visual nodes', () => {
const nodes = [
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
{ guid: { sessionID: 0, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' }, type: 'CANVAS', name: 'Page' },
{ guid: { sessionID: 0, localID: 10 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' }, type: 'FRAME', name: 'A', size: { x: 200, y: 100 }, transform: { m00: 1, m01: 0, m02: 500, m10: 0, m11: 1, m12: 300 } },
{ guid: { sessionID: 0, localID: 11 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' }, type: 'RECTANGLE', name: 'B', size: { x: 50, y: 50 }, transform: { m00: 1, m01: 0, m02: 800, m10: 0, m11: 1, m12: 400 } },
] as any[]
const bounds = figmaNodesBounds(nodes)
expect(bounds).toEqual({ x: 500, y: 300, w: 350, h: 150 })
})
it('ignores variables and non-visual types', () => {
const nodes = [
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
{ guid: { sessionID: 0, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' }, type: 'CANVAS', name: 'Page' },
{ guid: { sessionID: 0, localID: 2 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' }, type: 'VARIABLE_SET', name: 'Vars' },
{ guid: { sessionID: 0, localID: 10 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' }, type: 'FRAME', name: 'Card', size: { x: 300, y: 200 }, transform: { m00: 1, m01: 0, m02: 18000, m10: 0, m11: 1, m12: 45000 } },
] as any[]
const bounds = figmaNodesBounds(nodes)
expect(bounds).toEqual({ x: 18000, y: 45000, w: 300, h: 200 })
})
it('returns null for empty or all-non-visual nodes', () => {
expect(figmaNodesBounds([])).toBeNull()
const nodes = [
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
{ guid: { sessionID: 0, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' }, type: 'CANVAS', name: 'Page' },
] as any[]
expect(figmaNodesBounds(nodes)).toBeNull()
})
})