fix(editor): restore auto-layout instance dragging

This commit is contained in:
Danila Poyarkov 2026-05-13 04:47:28 +03:00
parent c38778094d
commit 08b9e56a15
3 changed files with 143 additions and 7 deletions

View file

@ -11,6 +11,26 @@ import { applyMoveSnap } from '#vue/shared/input/move-snap'
import type { DragMove } from '#vue/shared/input/types'
const AUTO_LAYOUT_REORDER_CLICK_SLOP = 3
const AUTO_LAYOUT_CROSS_AXIS_DRAG_TOLERANCE = 96
function isInsideAutoLayoutDragBounds(parentId: string, cx: number, cy: number, editor: Editor) {
const parent = editor.graph.getNode(parentId)
if (!parent) return false
const abs = editor.graph.getAbsolutePosition(parentId)
const isRow = parent.layoutMode === 'HORIZONTAL'
const mainStart = isRow ? abs.x : abs.y
const mainSize = isRow ? parent.width : parent.height
const crossStart = isRow ? abs.y : abs.x
const crossSize = isRow ? parent.height : parent.width
const main = isRow ? cx : cy
const cross = isRow ? cy : cx
return (
main >= mainStart - AUTO_LAYOUT_BREAK_THRESHOLD &&
main <= mainStart + mainSize + AUTO_LAYOUT_BREAK_THRESHOLD &&
cross >= crossStart - AUTO_LAYOUT_CROSS_AXIS_DRAG_TOLERANCE &&
cross <= crossStart + crossSize + AUTO_LAYOUT_CROSS_AXIS_DRAG_TOLERANCE
)
}
export function detectAutoLayoutParent(editor: Editor): string | undefined {
if (editor.state.selectedIds.size !== 1) return undefined
@ -32,8 +52,7 @@ export function handleMoveMove(d: DragMove, cx: number, cy: number, editor: Edit
let dy = cy - d.startY
if (d.autoLayoutParentId && !d.brokeFromAutoLayout) {
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < AUTO_LAYOUT_BREAK_THRESHOLD) {
if (isInsideAutoLayoutDragBounds(d.autoLayoutParentId, cx, cy, editor)) {
computeAutoLayoutIndicator(d, cx, cy, editor)
return
}
@ -84,7 +103,7 @@ export function handleMoveUp(d: DragMove, editor: Editor) {
editor.setDropTarget(null)
return
}
for (const id of editor.state.selectedIds) {
for (const id of d.originals.keys()) {
editor.reorderInAutoLayout(id, indicator.parentId, indicator.index)
}
editor.setDropTarget(null)

View file

@ -1,7 +1,6 @@
import type { Editor } from '@open-pencil/core/editor'
import { duplicateAndDrag } from '#vue/shared/input/duplicate-drag'
import { detectAutoLayoutParent } from '#vue/shared/input/move'
import type { DragState } from '#vue/shared/input/types'
type MoveOriginal = { x: number; y: number; parentId: string }
@ -10,9 +9,30 @@ export function selectionIsLocked(editor: Editor) {
return [...editor.state.selectedIds].every((id) => editor.graph.getNode(id)?.locked)
}
function autoLayoutMoveTarget(id: string, editor: Editor): string {
let current = editor.graph.getNode(id)
let target = current
while (current?.parentId) {
const parent = editor.graph.getNode(current.parentId)
if (!parent) break
if (
current.type === 'INSTANCE' &&
parent.layoutMode !== 'NONE' &&
current.layoutPositioning !== 'ABSOLUTE'
) {
target = current
}
current = parent
}
return target?.id ?? id
}
function collectMoveOriginals(editor: Editor) {
const originals = new Map<string, MoveOriginal>()
for (const id of editor.state.selectedIds) {
for (const selectedId of editor.state.selectedIds) {
const id = autoLayoutMoveTarget(selectedId, editor)
const node = editor.graph.getNode(id)
if (node) {
originals.set(id, {
@ -25,6 +45,17 @@ function collectMoveOriginals(editor: Editor) {
return originals
}
function detectDragAutoLayoutParent(originals: Map<string, MoveOriginal>, editor: Editor) {
if (originals.size !== 1) return undefined
const [id, original] = [...originals][0]
const node = editor.graph.getNode(id)
const parent = editor.graph.getNode(original.parentId)
if (parent && parent.layoutMode !== 'NONE' && node?.layoutPositioning !== 'ABSOLUTE') {
return parent.id
}
return undefined
}
export function createSelectionMoveDrag(
cx: number,
cy: number,
@ -33,13 +64,15 @@ export function createSelectionMoveDrag(
): DragState {
if (duplicate && editor.state.selectedIds.size > 0) return duplicateAndDrag(cx, cy, editor).drag
const originals = collectMoveOriginals(editor)
return {
type: 'move',
startX: cx,
startY: cy,
currentX: cx,
currentY: cy,
originals: collectMoveOriginals(editor),
autoLayoutParentId: detectAutoLayoutParent(editor)
originals,
autoLayoutParentId: detectDragAutoLayoutParent(originals, editor)
}
}

View file

@ -0,0 +1,84 @@
import { expect, test } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
test('dragging selected nested instance content reorders its auto-layout item', async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
canvas.errors.length = 0
await canvas.clearCanvas()
const ids = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.state.panX = 0
store.state.panY = 0
store.state.zoom = 1
const pageId = store.state.currentPageId
const frame = store.graph.createNode('FRAME', pageId, {
name: 'Auto Row',
x: 100,
y: 100,
width: 360,
height: 80,
layoutMode: 'HORIZONTAL',
itemSpacing: 20,
paddingTop: 10,
paddingRight: 10,
paddingBottom: 10,
paddingLeft: 10
})
const first = store.graph.createNode('INSTANCE', frame.id, {
name: 'First item',
x: 110,
y: 110,
width: 120,
height: 60
})
store.graph.createNode('TEXT', first.id, {
name: 'First label',
text: 'First',
x: 8,
y: 8,
width: 80,
height: 24
})
const second = store.graph.createNode('INSTANCE', frame.id, {
name: 'Second item',
x: 250,
y: 110,
width: 120,
height: 60
})
const secondText = store.graph.createNode('TEXT', second.id, {
name: 'Second label',
text: 'Second',
x: 8,
y: 8,
width: 80,
height: 24
})
store.select([secondText.id])
store.requestRender()
return { frame: frame.id, first: first.id, second: second.id, secondText: secondText.id }
})
await canvas.waitForRender()
await canvas.drag(370, 228, 230, 228, 12)
await canvas.waitForRender()
const childIds = await page.evaluate((frameId) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getNode(frameId)?.childIds ?? []
}, ids.frame)
expect(childIds).toEqual([ids.second, ids.first])
canvas.assertNoErrors()
})