feat(editor): add corner smoothing control

- Expose mixed smoothing percentages through Appearance controls
- Preserve per-node corner preview values in one-step undo
- Cover CanvasKit profiles and .fig import/export with engine and visual tests
This commit is contained in:
Danila Poyarkov 2026-07-17 19:28:34 +03:00
parent 2f8d799a5f
commit 1d8ddd951a
26 changed files with 383 additions and 29 deletions

View file

@ -16,6 +16,7 @@
- Scale the Layers panel to 5,000-node documents with virtualized rows, indexed updates, scroll-to-selection, range selection, and focus-aware themed states.
- Add Figma-style horizontal and vertical constraint controls with pin interactions, mixed-selection editing, undo, and responsive frame resizing.
- Add mixed-selection stroke cap, join, and miter-limit controls with CanvasKit rendering and `.fig` roundtrip support.
- Add a mixed-selection corner-smoothing percentage control with live preview, per-node undo restoration, and `.fig` roundtrip coverage.
- Standardize Vue SDK and app override type names on the `UI` acronym, including `FontPickerUI`.
- Add a headless Vue SDK NumberField with pointer scrubbing, keyboard stepping, safe arithmetic expressions, and mixed/bound states; remove the superseded ScrubInput API.
- Add provider-driven BindableValue primitives for variable and token binding, including detach-on-edit, read-only, edit-variable, mixed-value, and undo-batched interactions.

View file

@ -254,7 +254,7 @@ function serializeCornerRadii(node: SceneNode, nc: KiwiNodeChange): void {
nc.rectangleBottomLeftCornerRadius = node.bottomLeftRadius
nc.rectangleBottomRightCornerRadius = node.bottomRightRadius
}
if (node.cornerSmoothing > 0) {
if (node.cornerSmoothing > 0 || 'cornerSmoothing' in node.source.fig.rawNodeFields) {
nc.cornerSmoothing = node.cornerSmoothing
}
}

View file

@ -145,7 +145,7 @@ Figma's design documentation groups features into these areas:
| Effects: shadows and blurs | ✅ | ✅ | ✅ | ✅ | ✅ | `showShadowBehindNode` is rendered but not exposed in UI. |
| Effect styles | ↩ | — | — | ↩ | — | Style IDs round-trip; no style manager. |
| Corner radius | ✅ | ✅ | ✅ | ✅ | ✅ | Uniform and independent radii supported. |
| Corner smoothing | ✅ | ✅ | — | ✅ | ✅ | Figma-style smoothed corners render for common uniform and independent-radius rectangles; exact parity still needs broader fixture tuning. |
| Corner smoothing | ✅ | ✅ | ✅ | ✅ | ✅ | The inspector supports mixed smoothing percentages with undo; uniform and independent-radius corners render, while exact Figma parity still needs broader fixture tuning. |
| Masks | ✅ | ◐ | — | ✅ | ✅ | Figma schema `mask`, `maskType`, and `maskIsOutline` fields import and export; common sibling alpha/vector/luminance mask stacks render, including consecutive mask layers. UI controls and deeper Figma edge cases remain incomplete. |
| Auto layout: vertical/horizontal | ✅ | ✅ | ✅ | ✅ | ✅ | Yoga-backed layout. |
| Auto layout: wrap | ✅ | ✅ | ✅ | ✅ | ✅ | UI toggle exists. |

View file

@ -1,6 +1,6 @@
---
title: AppearanceControlsRoot
description: Headless root primitive for opacity, visibility, blend mode, and corner-radius controls.
description: Headless root primitive for opacity, visibility, blend mode, corner radius, and smoothing controls.
---
<script setup lang="ts">
@ -17,7 +17,11 @@ That state becomes active when the selected node explicitly uses independent cor
imported node contains unequal corner values with a stale uniform flag. Consumers should render
from this state rather than maintaining a parallel local expansion ref.
Multi-node independent-corner toggles and per-corner commits are grouped into one undo entry.
`cornerSmoothingPercent` exposes the normalized scene value as `0…100` or `MIXED`. Update it
through the corner actions using normalized `0…1` values.
Multi-node independent-corner toggles, smoothing edits, and per-corner commits are grouped into one
undo entry while preserving each node's original value.
## Generated API reference

View file

@ -1,6 +1,6 @@
---
title: useAppearance
description: Control visibility, opacity, and corner radius state for the current selection.
description: Control visibility, opacity, corner radius, and smoothing for the current selection.
---
# useAppearance
@ -12,6 +12,7 @@ It exposes selection-derived UI state for:
- visibility
- opacity
- corner radius
- corner smoothing as a normalized percentage
- independent corner radii, including imported unequal-corner state
- blend mode
@ -30,6 +31,7 @@ const {
visibilityState,
opacityPercent,
cornerRadiusValue,
cornerSmoothingPercent,
showIndependentCorners,
toggleVisibility,
toggleIndependentCorners,
@ -51,6 +53,13 @@ appearance.updateCornerProp('topLeftRadius', 12)
appearance.commitCornerProp('topLeftRadius', 12, 8)
```
### Edit corner smoothing
```ts
appearance.updateCornerProp('cornerSmoothing', 0.75)
appearance.commitCornerProp('cornerSmoothing', 0.75, 0)
```
Render the per-corner editor from `showIndependentCorners`. It accounts for both the explicit
scene-node flag and imported nodes whose corner values differ. Multi-selection toggles and commits
are grouped into one undo entry.

View file

@ -4,7 +4,7 @@ import type { ComputedRef } from 'vue'
import type { Editor } from '@open-pencil/core/editor'
import type { BlendMode, SceneNode } from '@open-pencil/scene-graph'
import type { CornerRadiusKey } from '#vue/controls/appearance/types'
import type { CornerGeometryKey } from '#vue/controls/appearance/types'
import { MIXED, type MixedValue } from '#vue/controls/node-props/use'
const CORNER_RADIUS_TYPES = new Set([
@ -56,6 +56,11 @@ export function createAppearanceState({ node, nodes, isMulti, merged }: Appearan
return node.value?.cornerRadius ?? 0
})
const cornerSmoothingPercent = computed(() => {
const value = merged('cornerSmoothing')
return value === MIXED ? MIXED : Math.round(Math.max(0, Math.min(value, 1)) * 100)
})
const opacityPercent = computed(() => {
const v = merged('opacity')
return v === MIXED ? MIXED : Math.round(v * 100)
@ -77,6 +82,7 @@ export function createAppearanceState({ node, nodes, isMulti, merged }: Appearan
independentCorners,
showIndependentCorners,
cornerRadiusValue,
cornerSmoothingPercent,
opacityPercent,
blendModeValue,
visibilityState
@ -84,6 +90,8 @@ export function createAppearanceState({ node, nodes, isMulti, merged }: Appearan
}
export function createAppearanceActions({ editor, node, nodes, isMulti }: AppearanceActionOptions) {
const previousCornerValues = new Map<CornerGeometryKey, Map<string, number>>()
function setBlendMode(value: BlendMode) {
const selected = node.value
const targets = isMulti.value ? nodes.value : []
@ -166,28 +174,40 @@ export function createAppearanceActions({ editor, node, nodes, isMulti }: Appear
)
}
function updateCornerProp(key: CornerRadiusKey, value: number) {
if (isMulti.value) {
for (const n of nodes.value) editor.updateNode(n.id, { [key]: value })
} else {
const n = node.value
if (n) editor.updateNode(n.id, { [key]: value })
function cornerTargets() {
if (isMulti.value) return nodes.value
const selected = node.value
return selected ? [selected] : []
}
function updateCornerProp(key: CornerGeometryKey, value: number) {
let snapshots = previousCornerValues.get(key)
if (!snapshots) {
snapshots = new Map()
previousCornerValues.set(key, snapshots)
}
const normalized = key === 'cornerSmoothing' ? Math.max(0, Math.min(value, 1)) : value
for (const target of cornerTargets()) {
if (!snapshots.has(target.id)) snapshots.set(target.id, target[key])
editor.updateNode(target.id, { [key]: normalized })
}
}
function commitCornerProp(key: CornerRadiusKey, _value: number, previous: number) {
if (isMulti.value) {
editor.undo.runBatch(`Change ${key}`, () => {
for (const n of nodes.value) {
editor.commitNodeUpdate(n.id, { [key]: previous } as Partial<SceneNode>, `Change ${key}`)
}
})
} else {
const n = node.value
if (n) {
editor.commitNodeUpdate(n.id, { [key]: previous } as Partial<SceneNode>, `Change ${key}`)
function commitCornerProp(key: CornerGeometryKey, _value: number, previous: number) {
const targets = cornerTargets()
const snapshots = previousCornerValues.get(key)
const commit = () => {
for (const target of targets) {
editor.commitNodeUpdate(
target.id,
{ [key]: snapshots?.get(target.id) ?? previous } as Partial<SceneNode>,
`Change ${key}`
)
}
}
if (targets.length > 1) editor.undo.runBatch(`Change ${key}`, commit)
else commit()
previousCornerValues.delete(key)
}
return {

View file

@ -3,3 +3,5 @@ export type CornerRadiusKey =
| 'topRightRadius'
| 'bottomRightRadius'
| 'bottomLeftRadius'
export type CornerGeometryKey = CornerRadiusKey | 'cornerSmoothing'

View file

@ -178,6 +178,7 @@
"height": "Höhe",
"opacity": "Deckkraft",
"radius": "Radius",
"cornerSmoothing": "Eckenglättung",
"spread": "Ausbreitung",
"foregroundBlur": "Vordergrund-Weichzeichnung",
"strokeType": "Linientyp",

View file

@ -20,6 +20,7 @@
"height": "Altura",
"opacity": "Opacidad",
"radius": "Redondeado",
"cornerSmoothing": "Suavizado de esquinas",
"spread": "Esparcimiento",
"fill": "Relleno",
"stroke": "Trazo",

View file

@ -178,6 +178,7 @@
"height": "Hauteur",
"opacity": "Opacité",
"radius": "Rayon",
"cornerSmoothing": "Lissage des coins",
"spread": "Diffusion",
"foregroundBlur": "Flou de premier plan",
"strokeType": "Type de contour",

View file

@ -178,6 +178,7 @@
"height": "Altezza",
"opacity": "Opacità",
"radius": "Raggio",
"cornerSmoothing": "Smussatura degli angoli",
"spread": "Diffusione",
"foregroundBlur": "Sfocatura primo piano",
"strokeType": "Tipo tratto",

View file

@ -20,6 +20,7 @@
"height": "高さ",
"opacity": "不透明度",
"radius": "角丸",
"cornerSmoothing": "コーナーの滑らかさ",
"spread": "スプレッド",
"fill": "塗り",
"stroke": "線",

View file

@ -178,6 +178,7 @@
"height": "Wysokość",
"opacity": "Krycie",
"radius": "Promień",
"cornerSmoothing": "Wygładzanie narożników",
"spread": "Rozproszenie",
"foregroundBlur": "Rozmycie pierwszego planu",
"strokeType": "Typ obrysu",

View file

@ -178,6 +178,7 @@
"height": "Высота",
"opacity": "Непрозрачность",
"radius": "Радиус",
"cornerSmoothing": "Сглаживание углов",
"spread": "Размах",
"foregroundBlur": "Размытие переднего плана",
"strokeType": "Тип обводки",

View file

@ -178,6 +178,7 @@
"height": "高度",
"opacity": "不透明度",
"radius": "圆角",
"cornerSmoothing": "圆角平滑度",
"spread": "扩展",
"foregroundBlur": "前景模糊",
"strokeType": "描边类型",

View file

@ -33,6 +33,7 @@ export const panelMessageDefaults = {
opacity: 'Opacity',
blendMode: 'Blend mode',
radius: 'Radius',
cornerSmoothing: 'Corner smoothing',
spread: 'Spread',
page: 'Page',

View file

@ -204,7 +204,7 @@ export {
useConstraints
} from '#vue/controls/constraints'
export type { ConstraintAxis, ConstraintEdge, ConstraintValue } from '#vue/controls/constraints'
export type { CornerRadiusKey } from '#vue/controls/appearance/types'
export type { CornerGeometryKey, CornerRadiusKey } from '#vue/controls/appearance/types'
export { PageListRoot } from '#vue/primitives/PageList'
export { PositionControlsRoot } from '#vue/primitives/PositionControls'
export { useEditorPropertyList } from '#vue/controls/property-list'

View file

@ -24,6 +24,7 @@ const actions = {
:independent-corners="ctx.independentCorners.value"
:show-independent-corners="ctx.showIndependentCorners.value"
:corner-radius-value="ctx.cornerRadiusValue.value"
:corner-smoothing-percent="ctx.cornerSmoothingPercent.value"
:opacity-percent="ctx.opacityPercent.value"
:blend-mode-value="ctx.blendModeValue.value"
:visibility-state="ctx.visibilityState.value"

View file

@ -2,7 +2,7 @@ import type { VNode } from 'vue'
import type { BlendMode, SceneNode } from '@open-pencil/scene-graph'
import type { CornerRadiusKey } from '#vue/controls/appearance/types'
import type { CornerGeometryKey } from '#vue/controls/appearance/types'
import type { MixedValue } from '#vue/controls/node-props/use'
export interface AppearanceControlsActions {
@ -11,8 +11,8 @@ export interface AppearanceControlsActions {
setBlendMode(value: BlendMode): void
toggleVisibility(): void
toggleIndependentCorners(): void
updateCornerProp(key: CornerRadiusKey, value: number): void
commitCornerProp(key: CornerRadiusKey, value: number, previous: number): void
updateCornerProp(key: CornerGeometryKey, value: number): void
commitCornerProp(key: CornerGeometryKey, value: number, previous: number): void
}
export interface AppearanceControlsRootSlotProps {
@ -23,6 +23,7 @@ export interface AppearanceControlsRootSlotProps {
independentCorners: MixedValue<boolean>
showIndependentCorners: boolean
cornerRadiusValue: MixedValue<number>
cornerSmoothingPercent: MixedValue<number>
opacityPercent: MixedValue<number>
blendModeValue: MixedValue<BlendMode>
visibilityState: 'visible' | 'hidden' | 'mixed'

View file

@ -54,6 +54,7 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
independentCorners,
showIndependentCorners,
cornerRadiusValue,
cornerSmoothingPercent,
opacityPercent,
blendModeValue,
visibilityState,
@ -221,6 +222,28 @@ function blendModeOptions(value: BlendMode | typeof MIXED) {
/>
<PanelRail />
</PanelGrid>
<PanelGrid v-if="hasCornerRadius" columns="fill" class="mt-panel">
<PanelFieldGroup :label="panels.cornerSmoothing">
<NumberField
suffix="%"
:model-value="cornerSmoothingPercent"
:min="0"
:max="100"
:aria-label="panels.cornerSmoothing"
data-property="corner-smoothing"
@update:model-value="actions.updateCornerProp('cornerSmoothing', $event / 100)"
@commit="
(v: number, p: number) =>
actions.commitCornerProp('cornerSmoothing', v / 100, p / 100)
"
>
<template #icon>
<icon-lucide-squircle class="size-3" />
</template>
</NumberField>
</PanelFieldGroup>
</PanelGrid>
</PanelSection>
</AppearanceControlsRoot>
</template>

View file

@ -0,0 +1,60 @@
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
test('regular partial full and independent corner smoothing', async () => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
const smoothingValues = [0, 0.5, 1]
for (const [index, cornerSmoothing] of smoothingValues.entries()) {
store.graph.createNode('RECTANGLE', pageId, {
name: `Corner smoothing ${cornerSmoothing}`,
x: 70 + index * 180,
y: 90,
width: 140,
height: 120,
cornerRadius: 42,
cornerSmoothing,
fills: [
{
type: 'SOLID',
color: { r: 0.23, g: 0.51, b: 0.96, a: 1 },
visible: true,
opacity: 1
}
]
})
}
store.graph.createNode('RECTANGLE', pageId, {
name: 'Independent smoothed corners',
x: 610,
y: 90,
width: 160,
height: 120,
independentCorners: true,
topLeftRadius: 50,
topRightRadius: 20,
bottomRightRadius: 50,
bottomLeftRadius: 8,
cornerSmoothing: 1,
fills: [
{
type: 'SOLID',
color: { r: 0.96, g: 0.35, b: 0.12, a: 1 },
visible: true,
opacity: 1
}
]
})
store.clearSelection()
store.requestRender()
})
await editor.canvas.waitForRender()
editor.canvas.assertNoErrors()
const buffer = await editor.canvas.canvas.screenshot()
expect(buffer).toMatchSnapshot('corner-smoothing-profiles.png')
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,103 @@
import { expect, test, type Page } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
import { propertySection } from '#tests/helpers/properties'
let page: Page
let canvas: CanvasHelper
test.describe.configure({ mode: 'serial' })
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await page.goto('/')
canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
test.afterAll(async () => {
await page.close()
})
async function drawFrame(x: number, y: number) {
await canvas.pressKey('f')
await canvas.drag(x, y, x + 120, y + 80)
await canvas.waitForRender()
}
async function selectedSmoothing() {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds].map(
(id) => store.graph.getNode(id)?.cornerSmoothing ?? null
)
})
}
async function fillSmoothing(value: string) {
const section = propertySection(page, 'Appearance')
await section.getByRole('spinbutton', { name: 'Corner smoothing' }).focus()
const input = section.getByRole('spinbutton', { name: 'Corner smoothing' })
await input.fill(value)
await input.press('Enter')
await canvas.waitForRender()
}
test('shows smoothing for corner-capable nodes and keeps it with independent radii', async () => {
await drawFrame(100, 100)
const smoothing = page.locator('[data-property="corner-smoothing"]')
await expect(smoothing).toBeVisible()
const independent = propertySection(page, 'Appearance').getByRole('button', {
name: 'Independent corner radii'
})
await independent.click()
await canvas.waitForRender()
await expect(page.locator('[data-corner-grid]')).toBeVisible()
await expect(smoothing).toBeVisible()
await canvas.pressKey('l')
await canvas.drag(300, 100, 420, 160)
await canvas.waitForRender()
await expect(smoothing).not.toBeVisible()
})
test('updates normalized smoothing and undoes the committed edit', async () => {
await drawFrame(100, 240)
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = [...store.state.selectedIds][0]
if (id) store.updateNode(id, { cornerRadius: 24 })
})
await fillSmoothing('75')
expect(await selectedSmoothing()).toEqual([0.75])
await canvas.pressKey('Meta+z')
await canvas.waitForRender()
expect(await selectedSmoothing()).toEqual([0])
})
test('restores mixed per-node values in one undo step', async () => {
await canvas.clearCanvas()
await drawFrame(80, 80)
await drawFrame(260, 80)
await canvas.pressKey('Meta+a')
await canvas.waitForRender()
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const ids = [...store.state.selectedIds]
if (ids[0]) store.updateNode(ids[0], { cornerRadius: 20, cornerSmoothing: 0.2 })
if (ids[1]) store.updateNode(ids[1], { cornerRadius: 20, cornerSmoothing: 0.8 })
})
await canvas.waitForRender()
await fillSmoothing('60')
expect(await selectedSmoothing()).toEqual([0.6, 0.6])
await canvas.pressKey('Meta+z')
await canvas.waitForRender()
expect(await selectedSmoothing()).toEqual([0.2, 0.8])
})

View file

@ -0,0 +1,40 @@
import { describe, expect, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/scene-graph'
import { sceneNodeToKiwi } from '#core/kiwi/fig/node-change/serialize'
function serializeCornerSmoothing(value: number, importedValue?: number) {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const node = graph.createNode('RECTANGLE', page.id, { cornerRadius: 24, cornerSmoothing: value })
if (importedValue !== undefined) {
graph.updateNode(node.id, {
source: {
...node.source,
id: '1:2',
fig: {
...node.source.fig,
rawNodeFields: {
...node.source.fig.rawNodeFields,
cornerSmoothing: importedValue
}
}
}
})
}
const current = graph.getNode(node.id)
if (!current) throw new Error('Expected rectangle node')
return sceneNodeToKiwi(current, { sessionID: 1, localID: 1 }, 0, { value: 2 }, graph, [])[0]
.cornerSmoothing
}
describe('Figma corner smoothing export', () => {
test('exports normalized smoothing values', () => {
expect(serializeCornerSmoothing(0.72)).toBe(0.72)
})
test('overrides stale imported smoothing when edited back to zero', () => {
expect(serializeCornerSmoothing(0, 0.72)).toBe(0)
})
})

View file

@ -0,0 +1,22 @@
import { describe, expect, test } from 'bun:test'
import { importNodeChanges } from '@open-pencil/core'
import type { NodeChange } from '@open-pencil/kiwi/fig/codec'
import { canvas, doc, node } from './helpers'
describe('fig-import: corner geometry', () => {
test('imports corner smoothing', () => {
const graph = importNodeChanges([
doc(),
canvas(),
node('RECTANGLE', 10, 1, {
cornerRadius: 24,
cornerSmoothing: 0.72
} as Partial<NodeChange>)
])
const imported = graph.getChildren(graph.getPages()[0].id)[0]
expect(imported.cornerRadius).toBe(24)
expect(imported.cornerSmoothing).toBe(0.72)
})
})

View file

@ -2,10 +2,11 @@ import { describe, expect, test } from 'bun:test'
import { computed, ref } from 'vue'
import { createEditor } from '@open-pencil/core/editor'
import type { SceneNode } from '@open-pencil/scene-graph'
import type { MixedValue } from '@open-pencil/vue'
import { MIXED, type MixedValue } from '@open-pencil/vue'
import { createAppearanceState } from '#vue/controls/appearance/helpers'
import { createAppearanceActions, createAppearanceState } from '#vue/controls/appearance/helpers'
import { createRect, firstPageId, makeSceneGraph } from '#tests/helpers/scene'
@ -61,4 +62,62 @@ describe('appearance control state', () => {
const state = appearanceState(node, true)
expect(state.showIndependentCorners.value).toBe(false)
})
test('presents normalized corner smoothing as a percentage', () => {
const node = rectangle()
node.cornerSmoothing = 0.735
const state = appearanceState(node)
expect(state.cornerSmoothingPercent.value).toBe(74)
})
test('keeps independent corner preview and undo behavior', () => {
const graph = makeSceneGraph()
const pageId = firstPageId(graph)
const rect = graph.createNode('RECTANGLE', pageId, {
independentCorners: true,
topLeftRadius: 8
})
const editor = createEditor({ graph })
const actions = createAppearanceActions({
editor,
node: computed(() => graph.getNode(rect.id) ?? null),
nodes: computed(() => []),
isMulti: computed(() => false),
merged: (key) => graph.getNode(rect.id)?.[key] ?? MIXED
})
actions.updateCornerProp('topLeftRadius', 20)
actions.commitCornerProp('topLeftRadius', 20, 8)
expect(graph.getNode(rect.id)?.topLeftRadius).toBe(20)
editor.undo.undo()
expect(graph.getNode(rect.id)?.topLeftRadius).toBe(8)
})
test('restores each mixed smoothing value in one undo step', () => {
const graph = makeSceneGraph()
const pageId = firstPageId(graph)
const first = graph.createNode('RECTANGLE', pageId, { cornerSmoothing: 0.2 })
const second = graph.createNode('RECTANGLE', pageId, { cornerSmoothing: 0.8 })
const editor = createEditor({ graph })
const nodes = computed(() => {
const selected = [graph.getNode(first.id), graph.getNode(second.id)]
return selected.filter((value): value is SceneNode => value !== undefined)
})
const actions = createAppearanceActions({
editor,
node: computed(() => null),
nodes,
isMulti: computed(() => true),
merged: () => MIXED
})
actions.updateCornerProp('cornerSmoothing', 1.4)
expect(graph.getNode(first.id)?.cornerSmoothing).toBe(1)
expect(graph.getNode(second.id)?.cornerSmoothing).toBe(1)
actions.commitCornerProp('cornerSmoothing', 1, 0)
editor.undo.undo()
expect(graph.getNode(first.id)?.cornerSmoothing).toBe(0.2)
expect(graph.getNode(second.id)?.cornerSmoothing).toBe(0.8)
})
})