fix(app): restore left panel scrolling
This commit is contained in:
parent
8cf51ffefd
commit
9ab6bfa3bf
|
|
@ -6,6 +6,7 @@ import { createNanoEvents } from 'nanoevents'
|
|||
import * as HitTest from './hit-test'
|
||||
import * as Instances from './instances'
|
||||
import { CONTAINER_TYPES, createDefaultNode } from './node-defaults'
|
||||
import { updateNodePreview } from './preview'
|
||||
import * as Variables from './variables'
|
||||
import { normalizeVectorNetwork } from './vector-network'
|
||||
|
||||
|
|
@ -363,25 +364,7 @@ export class SceneGraph {
|
|||
}
|
||||
|
||||
updateNodePreview(id: string, changes: Partial<SceneNode>): void {
|
||||
const node = this.nodes.get(id)
|
||||
if (!node) return
|
||||
if ((Object.keys(changes) as (keyof SceneNode)[]).every((key) => node[key] === changes[key])) {
|
||||
return
|
||||
}
|
||||
const affectsLayout = Object.keys(changes).some((k) => SceneGraph.LAYOUT_AFFECTING_KEYS.has(k))
|
||||
if (affectsLayout) this.absPosCache.clear()
|
||||
if (
|
||||
node.type === 'TEXT' &&
|
||||
node.textPicture &&
|
||||
Object.keys(changes).some((k) => SceneGraph.TEXT_PICTURE_KEYS.has(k))
|
||||
) {
|
||||
node.textPicture = null
|
||||
}
|
||||
if (changes.vectorNetwork) {
|
||||
changes = { ...changes, vectorNetwork: normalizeVectorNetwork(changes.vectorNetwork) }
|
||||
}
|
||||
this.positionPreviewVersion++
|
||||
Object.assign(node, changes)
|
||||
updateNodePreview(this, id, changes)
|
||||
}
|
||||
|
||||
updateNode(id: string, changes: Partial<SceneNode>): void {
|
||||
|
|
|
|||
89
packages/core/src/scene-graph/preview.ts
Normal file
89
packages/core/src/scene-graph/preview.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { normalizeVectorNetwork } from './vector-network'
|
||||
|
||||
import type { SceneNode } from './types'
|
||||
|
||||
type PreviewGraph = {
|
||||
nodes: Map<string, SceneNode>
|
||||
positionPreviewVersion: number
|
||||
clearAbsPosCache: () => void
|
||||
}
|
||||
|
||||
const LAYOUT_AFFECTING_KEYS = new Set<string>([
|
||||
'x',
|
||||
'y',
|
||||
'width',
|
||||
'height',
|
||||
'rotation',
|
||||
'parentId',
|
||||
'childIds',
|
||||
'layoutMode',
|
||||
'layoutDirection',
|
||||
'layoutWrap',
|
||||
'primaryAxisSizing',
|
||||
'counterAxisSizing',
|
||||
'itemSpacing',
|
||||
'counterAxisSpacing',
|
||||
'paddingTop',
|
||||
'paddingRight',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
'layoutGrow',
|
||||
'layoutAlignSelf',
|
||||
'layoutPositioning',
|
||||
'minWidth',
|
||||
'maxWidth',
|
||||
'minHeight',
|
||||
'maxHeight',
|
||||
'visible',
|
||||
'text',
|
||||
'fontSize',
|
||||
'lineHeight',
|
||||
'letterSpacing',
|
||||
'styleRuns',
|
||||
'textAutoResize'
|
||||
])
|
||||
|
||||
const TEXT_PICTURE_KEYS = new Set<string>([
|
||||
'text',
|
||||
'fontSize',
|
||||
'fontFamily',
|
||||
'fontWeight',
|
||||
'italic',
|
||||
'textAlignHorizontal',
|
||||
'textDirection',
|
||||
'textAlignVertical',
|
||||
'lineHeight',
|
||||
'letterSpacing',
|
||||
'textDecoration',
|
||||
'textCase',
|
||||
'styleRuns',
|
||||
'fills',
|
||||
'width',
|
||||
'height'
|
||||
])
|
||||
|
||||
export function updateNodePreview(
|
||||
graph: PreviewGraph,
|
||||
id: string,
|
||||
changes: Partial<SceneNode>
|
||||
): void {
|
||||
const node = graph.nodes.get(id)
|
||||
if (!node) return
|
||||
if ((Object.keys(changes) as (keyof SceneNode)[]).every((key) => node[key] === changes[key])) {
|
||||
return
|
||||
}
|
||||
const affectsLayout = Object.keys(changes).some((key) => LAYOUT_AFFECTING_KEYS.has(key))
|
||||
if (affectsLayout) graph.clearAbsPosCache()
|
||||
if (
|
||||
node.type === 'TEXT' &&
|
||||
node.textPicture &&
|
||||
Object.keys(changes).some((key) => TEXT_PICTURE_KEYS.has(key))
|
||||
) {
|
||||
node.textPicture = null
|
||||
}
|
||||
const normalizedChanges = changes.vectorNetwork
|
||||
? { ...changes, vectorNetwork: normalizeVectorNetwork(changes.vectorNetwork) }
|
||||
: changes
|
||||
graph.positionPreviewVersion++
|
||||
Object.assign(node, normalizedChanges)
|
||||
}
|
||||
|
|
@ -163,6 +163,8 @@ provideLayerTree({
|
|||
<TreeRoot
|
||||
:key="treeKey"
|
||||
v-slot="{ flattenItems }"
|
||||
as="div"
|
||||
class="flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||
:expanded="expanded"
|
||||
:items="items"
|
||||
:get-key="getKey"
|
||||
|
|
|
|||
|
|
@ -50,8 +50,9 @@ function onTreeSelect(e: CustomEvent, select: (additive: boolean) => void) {
|
|||
:indent-per-level="INDENT"
|
||||
>
|
||||
<ContextMenuRoot :modal="false">
|
||||
<ContextMenuTrigger as-child @contextmenu="onLayerRightClick">
|
||||
<div v-bind="attrs" class="relative scrollbar-thin flex-1 overflow-y-auto px-1">
|
||||
<div v-bind="attrs" class="relative min-h-0 flex-1 overflow-hidden">
|
||||
<ContextMenuTrigger as-child @contextmenu="onLayerRightClick">
|
||||
<div data-test-id="layers-scroll" class="scrollbar-thin h-full overflow-y-auto px-1">
|
||||
<template v-if="flattenItems">
|
||||
<LayerTreeItem
|
||||
v-for="item in flattenItems"
|
||||
|
|
@ -211,8 +212,9 @@ function onTreeSelect(e: CustomEvent, select: (additive: boolean) => void) {
|
|||
</TreeItem>
|
||||
</LayerTreeItem>
|
||||
</template>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
</div>
|
||||
<ContextMenuPortal>
|
||||
<CanvasMenu />
|
||||
</ContextMenuPortal>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ function handlePageDblClick(
|
|||
</button>
|
||||
</Tip>
|
||||
</div>
|
||||
<div class="scrollbar-thin overflow-x-hidden overflow-y-auto px-1 pb-1">
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
<div data-test-id="pages-scroll" class="scrollbar-thin h-full overflow-x-hidden overflow-y-auto px-1 pb-1">
|
||||
<div v-for="pg in pages" :key="pg.id">
|
||||
<div
|
||||
v-if="rename.editingId.value === pg.id"
|
||||
|
|
@ -91,6 +92,7 @@ function handlePageDblClick(
|
|||
<span class="truncate">{{ pg.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageListRoot>
|
||||
|
|
|
|||
42
tests/e2e/layers/scroll.spec.ts
Normal file
42
tests/e2e/layers/scroll.spec.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
|
||||
test('pages and layers panels scroll inside splitter panes', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
canvas.errors.length = 0
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
for (let i = 0; i < 40; i++) store.addPage(`Page ${i + 2}`)
|
||||
const pageId = store.state.currentPageId
|
||||
for (let i = 0; i < 80; i++) {
|
||||
store.graph.createNode('RECTANGLE', pageId, {
|
||||
name: `Layer ${i + 1}`,
|
||||
x: i * 2,
|
||||
y: i * 2,
|
||||
width: 20,
|
||||
height: 20
|
||||
})
|
||||
}
|
||||
store.requestRender()
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
const pagesScroller = page.locator('[data-test-id="pages-scroll"]')
|
||||
const layersScroller = page.locator('[data-test-id="layers-scroll"]')
|
||||
|
||||
await pagesScroller.evaluate((el) => {
|
||||
el.scrollTop = el.scrollHeight
|
||||
})
|
||||
await layersScroller.evaluate((el) => {
|
||||
el.scrollTop = el.scrollHeight
|
||||
})
|
||||
|
||||
await expect.poll(() => pagesScroller.evaluate((el) => el.scrollTop)).toBeGreaterThan(0)
|
||||
await expect.poll(() => layersScroller.evaluate((el) => el.scrollTop)).toBeGreaterThan(0)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
Loading…
Reference in a new issue