Release v0.14.0

This commit is contained in:
Danila Poyarkov 2026-08-10 11:22:59 +03:00
parent f972efe229
commit 751195891f
19 changed files with 65 additions and 31 deletions

View file

@ -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.

2
desktop/Cargo.lock generated
View file

@ -2939,7 +2939,7 @@ dependencies = [
[[package]]
name = "open_pencil"
version = "0.13.2"
version = "0.14.0"
dependencies = [
"fix-path-env",
"font-kit",

View file

@ -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"

View file

@ -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",

View file

@ -1,6 +1,6 @@
{
"name": "open-pencil-app",
"version": "0.13.2",
"version": "0.14.0",
"private": true,
"license": "MIT",
"workspaces": [

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/cli",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"imports": {

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/core",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"repository": {
"type": "git",

View file

@ -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' })

View file

@ -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<string>()
computeLayoutsBottomUp(graph, rootId, visited)
if (applyEffectiveGeneratedTextLayout(graph, rootId)) {
computeLayoutsBottomUp(graph, rootId, new Set())
}
graph.withLayoutMutations(() => {
const rootId = scopeId ?? graph.rootId
const visited = new Set<string>()
computeLayoutsBottomUp(graph, rootId, visited)
if (applyEffectiveGeneratedTextLayout(graph, rootId)) {
computeLayoutsBottomUp(graph, rootId, new Set())
}
})
}
function computeLayoutsBottomUp(graph: SceneGraph, nodeId: string, visited: Set<string>): void {

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/dom-css",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"imports": {

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/fig",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"exports": {

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/kiwi",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"exports": {

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/mcp",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"imports": {

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/pen",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"exports": {

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/scene-graph",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"exports": {

View file

@ -78,6 +78,7 @@ export class SceneGraph {
private absPosCache = new Map<string, Vector>()
private previewMutationDepth = 0
private sourceMetadataPreservationDepth = 0
private layoutMutationDepth = 0
positionPreviewVersion = 0
instanceIndex = new Map<string, Set<string>>()
@ -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 })
}

View file

@ -1,6 +1,6 @@
{
"name": "@open-pencil/vue",
"version": "0.13.2",
"version": "0.14.0",
"license": "MIT",
"type": "module",
"exports": {

View file

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB