From 751195891ff7ee6730a44f1786b719a309eb270e Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 10 Aug 2026 11:22:59 +0300 Subject: [PATCH] Release v0.14.0 --- CHANGELOG.md | 2 ++ desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- .../core/src/kiwi/fig/population/client.ts | 11 +++++--- packages/core/src/layout.ts | 20 +++++++++----- packages/dom-css/package.json | 2 +- packages/fig/package.json | 2 +- packages/kiwi/package.json | 2 +- packages/mcp/package.json | 2 +- packages/pen/package.json | 2 +- packages/scene-graph/package.json | 2 +- packages/scene-graph/src/index.ts | 12 +++++++++ packages/vue/package.json | 2 +- tests/e2e/pages/fig-population-worker.spec.ts | 25 +++++++++++++----- .../effect-blend-mode-openpencil-darwin.png | Bin 7446 -> 7773 bytes 19 files changed, 65 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d220c41b8..4b9b2a087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.14.0 - 2026-08-07 + ### Breaking changes - **Core SDK:** Import scene graph types, geometry, coordinate, matrix, snapping, undo, and path helpers from `@open-pencil/scene-graph`; import `.pen` parsing from `@open-pencil/pen`; and import synchronous Kiwi decompression from `@open-pencil/kiwi` instead of the `@open-pencil/core` compatibility barrel. diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 5da560134..2dfb2c578 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2939,7 +2939,7 @@ dependencies = [ [[package]] name = "open_pencil" -version = "0.13.2" +version = "0.14.0" dependencies = [ "fix-path-env", "font-kit", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 429bf9a1f..8f969b9a4 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open_pencil" -version = "0.13.2" +version = "0.14.0" description = "OpenPencil desktop app" authors = ["Danila Poyarkov"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 7dd411121..1497a4e28 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenPencil", - "version": "0.13.2", + "version": "0.14.0", "identifier": "net.dannote.open-pencil", "build": { "beforeDevCommand": "bun run generate:tauri-menu && bun run dev", diff --git a/package.json b/package.json index b621f7177..78f2b88ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-pencil-app", - "version": "0.13.2", + "version": "0.14.0", "private": true, "license": "MIT", "workspaces": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index acd5bda91..6ef731530 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/cli", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "imports": { diff --git a/packages/core/package.json b/packages/core/package.json index fd46df11a..e5b81f2ca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/core", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/core/src/kiwi/fig/population/client.ts b/packages/core/src/kiwi/fig/population/client.ts index f806502b9..459a709aa 100644 --- a/packages/core/src/kiwi/fig/population/client.ts +++ b/packages/core/src/kiwi/fig/population/client.ts @@ -44,13 +44,13 @@ export function registerFigPopulationWorker(graph: SceneGraph, worker: Worker): emitTelemetry({ event: 'registered' }) } -function isDevelopmentBuild(meta: { env?: { DEV?: boolean } }): boolean { - return meta.env?.DEV ?? false +function isDevelopmentBuild(env?: { DEV?: boolean }): boolean { + return env?.DEV ?? false } export function canUseFigPopulationWorker(graph: SceneGraph): boolean { return ( - isDevelopmentBuild(import.meta) && + isDevelopmentBuild(import.meta.env) && populationWorkers.has(graph) && getLazyFigImportContext(graph) !== undefined ) @@ -81,7 +81,10 @@ function createPopulationWorkerClient(graph: SceneGraph, worker: Worker): FigPop let disposed = false let applyingDelta = false const invalidate = () => { - if (applyingDelta || stale) return + // Layout recomputation (import-time or after a switch) is derived from the + // same scene graph the worker deltas were built from; it must not count as + // user divergence. Only real user edits invalidate the worker. + if (applyingDelta || stale || graph.isApplyingLayout) return revision++ stale = true emitTelemetry({ event: 'stale', reason: 'graph-mutation' }) diff --git a/packages/core/src/layout.ts b/packages/core/src/layout.ts index 1aae2c2fa..d93476c8c 100644 --- a/packages/core/src/layout.ts +++ b/packages/core/src/layout.ts @@ -37,6 +37,10 @@ import { } from './layout/yoga-helpers' export function computeLayout(graph: SceneGraph, frameId: string): void { + graph.withLayoutMutations(() => computeLayoutInternal(graph, frameId)) +} + +function computeLayoutInternal(graph: SceneGraph, frameId: string): void { const frame = graph.getNode(frameId) if (!frame || frame.layoutMode === 'NONE') return @@ -47,7 +51,7 @@ export function computeLayout(graph: SceneGraph, frameId: string): void { ? buildGridTree(graph, frame, rootDirection) : buildYogaTree(graph, frame, rootDirection) yogaRoot.calculateLayout(undefined, undefined, yogaDirection) - applyYogaLayout(graph, frame, yogaRoot, computeLayout) + applyYogaLayout(graph, frame, yogaRoot, computeLayoutInternal) freeYogaTree(yogaRoot) } function resolveComputedLayoutDirection( @@ -60,12 +64,14 @@ function resolveComputedLayoutDirection( } export function computeAllLayouts(graph: SceneGraph, scopeId?: string): void { - const rootId = scopeId ?? graph.rootId - const visited = new Set() - computeLayoutsBottomUp(graph, rootId, visited) - if (applyEffectiveGeneratedTextLayout(graph, rootId)) { - computeLayoutsBottomUp(graph, rootId, new Set()) - } + graph.withLayoutMutations(() => { + const rootId = scopeId ?? graph.rootId + const visited = new Set() + computeLayoutsBottomUp(graph, rootId, visited) + if (applyEffectiveGeneratedTextLayout(graph, rootId)) { + computeLayoutsBottomUp(graph, rootId, new Set()) + } + }) } function computeLayoutsBottomUp(graph: SceneGraph, nodeId: string, visited: Set): void { diff --git a/packages/dom-css/package.json b/packages/dom-css/package.json index 46cc37c5f..a9b67c56e 100644 --- a/packages/dom-css/package.json +++ b/packages/dom-css/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/dom-css", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "imports": { diff --git a/packages/fig/package.json b/packages/fig/package.json index 70d280142..3b5787c50 100644 --- a/packages/fig/package.json +++ b/packages/fig/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/fig", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "exports": { diff --git a/packages/kiwi/package.json b/packages/kiwi/package.json index a0dcc4aee..d0b2c5e5c 100644 --- a/packages/kiwi/package.json +++ b/packages/kiwi/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/kiwi", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "exports": { diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 8655a7c07..b2952f4c7 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/mcp", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "imports": { diff --git a/packages/pen/package.json b/packages/pen/package.json index 5d14d6ce1..462ee3017 100644 --- a/packages/pen/package.json +++ b/packages/pen/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/pen", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "exports": { diff --git a/packages/scene-graph/package.json b/packages/scene-graph/package.json index 72a8e8b94..f32f0157e 100644 --- a/packages/scene-graph/package.json +++ b/packages/scene-graph/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/scene-graph", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "exports": { diff --git a/packages/scene-graph/src/index.ts b/packages/scene-graph/src/index.ts index 0b0015bb3..50d8241f2 100644 --- a/packages/scene-graph/src/index.ts +++ b/packages/scene-graph/src/index.ts @@ -78,6 +78,7 @@ export class SceneGraph { private absPosCache = new Map() private previewMutationDepth = 0 private sourceMetadataPreservationDepth = 0 + private layoutMutationDepth = 0 positionPreviewVersion = 0 instanceIndex = new Map>() @@ -366,6 +367,17 @@ export class SceneGraph { this.sourceMetadataPreservationDepth-- } } + withLayoutMutations(fn: () => void): void { + this.layoutMutationDepth++ + try { + fn() + } finally { + this.layoutMutationDepth-- + } + } + get isApplyingLayout(): boolean { + return this.layoutMutationDepth > 0 + } updateNodePositionPreview(id: string, x: number, y: number): void { this.updateNodePreview(id, { x, y }) } diff --git a/packages/vue/package.json b/packages/vue/package.json index 012461515..65983c069 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@open-pencil/vue", - "version": "0.13.2", + "version": "0.14.0", "license": "MIT", "type": "module", "exports": { diff --git a/tests/e2e/pages/fig-population-worker.spec.ts b/tests/e2e/pages/fig-population-worker.spec.ts index a585a1005..dfc1fc095 100644 --- a/tests/e2e/pages/fig-population-worker.spec.ts +++ b/tests/e2e/pages/fig-population-worker.spec.ts @@ -3,6 +3,7 @@ import { expect, test, useEditorSetup } from '#tests/e2e/fixtures' const editor = useEditorSetup() test('populates a real lazy FIG page in the retained parse worker', async () => { + test.setTimeout(90_000) await editor.page.evaluate(() => { const events: unknown[] = [] Object.assign(window, { figPopulationWorkerEvents: events }) @@ -11,7 +12,7 @@ test('populates a real lazy FIG page in the retained parse worker', async () => }) }) const openFile = editor.page.evaluate(() => - window.openPencil?.openFile?.('/tests/fixtures/gold-preview.fig') + window.openPencil?.openFile?.('/tests/fixtures/material3.fig') ) await openFile await editor.canvas.waitForRender() @@ -23,18 +24,28 @@ test('populates a real lazy FIG page in the retained parse worker', async () => }) if (!targetPageId) throw new Error('Target page not found') + const populateEventCount = await editor.page.evaluate( + () => + (Reflect.get(window, 'figPopulationWorkerEvents') as Array<{ event: string }>).filter( + ({ event }) => event === 'populate' + ).length + ) await editor.page.evaluate((pageId) => { const store = window.openPencil?.getStore?.() if (!store) throw new Error('OpenPencil store not initialized') return store.switchPage(pageId) }, targetPageId) await expect - .poll(() => - editor.page.evaluate(() => - (Reflect.get(window, 'figPopulationWorkerEvents') as Array<{ event: string }>).some( - ({ event }) => event === 'populate' - ) - ) + .poll( + () => + editor.page.evaluate( + (previousCount) => + (Reflect.get(window, 'figPopulationWorkerEvents') as Array<{ event: string }>).filter( + ({ event }) => event === 'populate' + ).length > previousCount, + populateEventCount + ), + { timeout: 45_000 } ) .toBe(true) diff --git a/tests/e2e/properties/blend-modes.spec.ts-snapshots/effect-blend-mode-openpencil-darwin.png b/tests/e2e/properties/blend-modes.spec.ts-snapshots/effect-blend-mode-openpencil-darwin.png index 7b26f04dcd2a9d48a93fb272e95a8c957055b5de..a77adbc95ed2ba5b989cfe695d650feab9f84323 100644 GIT binary patch literal 7773 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 s=F%