Center pasted Figma nodes in viewport instead of using original coordinates
This commit is contained in:
parent
d7ba0bfef9
commit
8b1547ece7
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export {
|
|||
export {
|
||||
parseFigmaClipboard,
|
||||
importClipboardNodes,
|
||||
figmaNodesBounds,
|
||||
parseOpenPencilClipboard,
|
||||
buildFigmaClipboardHTML,
|
||||
buildOpenPencilClipboardHTML,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export {
|
||||
parseFigmaClipboard,
|
||||
importClipboardNodes,
|
||||
figmaNodesBounds,
|
||||
parseOpenPencilClipboard,
|
||||
buildFigmaClipboardHTML,
|
||||
buildOpenPencilClipboardHTML,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue