From 9ab6bfa3bf6d4c31cbfe1590af4b10d865dd9e66 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 13 May 2026 05:47:32 +0300 Subject: [PATCH] fix(app): restore left panel scrolling --- packages/core/src/scene-graph/index.ts | 21 +---- packages/core/src/scene-graph/preview.ts | 89 +++++++++++++++++++ .../primitives/LayerTree/LayerTreeRoot.vue | 2 + src/components/LayerTree.vue | 10 ++- src/components/PagesPanel.vue | 4 +- tests/e2e/layers/scroll.spec.ts | 42 +++++++++ 6 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/scene-graph/preview.ts create mode 100644 tests/e2e/layers/scroll.spec.ts diff --git a/packages/core/src/scene-graph/index.ts b/packages/core/src/scene-graph/index.ts index 116df2f86..e4a0c8b52 100644 --- a/packages/core/src/scene-graph/index.ts +++ b/packages/core/src/scene-graph/index.ts @@ -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): 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): void { diff --git a/packages/core/src/scene-graph/preview.ts b/packages/core/src/scene-graph/preview.ts new file mode 100644 index 000000000..72e569e03 --- /dev/null +++ b/packages/core/src/scene-graph/preview.ts @@ -0,0 +1,89 @@ +import { normalizeVectorNetwork } from './vector-network' + +import type { SceneNode } from './types' + +type PreviewGraph = { + nodes: Map + positionPreviewVersion: number + clearAbsPosCache: () => void +} + +const LAYOUT_AFFECTING_KEYS = new Set([ + '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([ + '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 +): 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) +} diff --git a/packages/vue/src/primitives/LayerTree/LayerTreeRoot.vue b/packages/vue/src/primitives/LayerTree/LayerTreeRoot.vue index d317afa4a..b9dce471e 100644 --- a/packages/vue/src/primitives/LayerTree/LayerTreeRoot.vue +++ b/packages/vue/src/primitives/LayerTree/LayerTreeRoot.vue @@ -163,6 +163,8 @@ provideLayerTree({ void) { :indent-per-level="INDENT" > - -
+
+ +
-
-
+
+ +
diff --git a/src/components/PagesPanel.vue b/src/components/PagesPanel.vue index f7b6d3be2..0a63aa795 100644 --- a/src/components/PagesPanel.vue +++ b/src/components/PagesPanel.vue @@ -52,7 +52,8 @@ function handlePageDblClick( -
+
+
{{ pg.name }}
+
diff --git a/tests/e2e/layers/scroll.spec.ts b/tests/e2e/layers/scroll.spec.ts new file mode 100644 index 000000000..38406660d --- /dev/null +++ b/tests/e2e/layers/scroll.spec.ts @@ -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() +})