feat(editor): add paint blend mode controls
- Expose localized blend mode selects for fills and expanded effects - Reuse PropertyList batching for shared-style detachment and mixed-selection undo - Share Appearance, Fill, and Effects blend mode options - Add focused E2E and panel visual coverage
This commit is contained in:
parent
23645d5a2d
commit
0bd5afe784
|
|
@ -19,6 +19,7 @@
|
|||
- Add a mixed-selection corner-smoothing percentage control with live preview, per-node undo restoration, and `.fig` roundtrip coverage.
|
||||
- Model imported fill, stroke, text, effect, and grid styles with reusable SDK/app selectors, automatic detach-on-edit, undo, and `.fig` definition roundtrips.
|
||||
- Add variant, text, boolean, and nested instance-swap component property controls with mixed-selection undo and typed `.fig` metadata roundtrips.
|
||||
- Add fill and effect blend-mode controls with shared-style detachment and mixed-selection undo.
|
||||
- Standardize Pages, Layers/Assets navigation, and document tab states across light and dark themes.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ Figma's design documentation groups features into these areas:
|
|||
| Image fills | ✅ | ✅ | ◐ | ✅ | ✅ | Fill/fit/crop/tile support exists; imported crop/tile affine transforms are applied, but exact Figma parity is still partial. |
|
||||
| Pattern / noise / custom fills | ✅ | ◐ | — | ✅ | — | Schema metadata imports/exports; Figma pattern fills with a referenced source node render as repeated source tiles with scale, spacing, alignment, and basic hex offsets. Noise/custom paints still render with a solid fallback pending real paint payload samples; Figma-authored noise/texture/glass effect payloads are captured separately. |
|
||||
| Video/GIF/media fills | ↩ | — | — | ↩ | — | Kiwi schema includes media paint/export enums, but OpenPencil has no video/GIF playback or media layer support. |
|
||||
| Layer/fill/effect blend modes | ✅ | ◐ | — | ✅ | ✅ | Canvas applies node, fill, and common shadow effect blend modes; Figma isolation edge cases remain partial. |
|
||||
| Layer/fill/effect blend modes | ✅ | ◐ | ✅ | ✅ | ✅ | Appearance, fill, and effect controls are exposed; Canvas applies common modes, while Figma isolation edge cases remain partial. |
|
||||
| Opacity | ✅ | ✅ | ✅ | ✅ | ✅ | Node opacity uses save layers in the renderer. |
|
||||
| Strokes | ✅ | ✅ | ✅ | ✅ | ✅ | Weight, alignment, dashes, and side weights are supported. |
|
||||
| Stroke caps / joins / miter limit | ✅ | ✅ | ✅ | ✅ | ✅ | Inspector controls support mixed cap/join/miter editing; CanvasKit rendering and `.fig` roundtrips preserve miter limits. |
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { AppearanceControlsRoot, MIXED, useI18n } from '@open-pencil/vue'
|
||||
|
||||
import NumberField from '@/components/inputs/NumberField.vue'
|
||||
import VariableNumberField from '@/components/properties/VariableNumberField.vue'
|
||||
import { useBlendModeOptions } from '@/components/properties/blend-mode/use'
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
||||
|
|
@ -17,25 +16,7 @@ import type { BlendMode } from '@open-pencil/scene-graph'
|
|||
const { panels } = useI18n()
|
||||
type BlendModeSelectValue = BlendMode | 'MIXED'
|
||||
|
||||
const baseBlendModeOptions = computed<Array<{ value: BlendModeSelectValue; label: string }>>(() => [
|
||||
{ value: 'PASS_THROUGH', label: panels.value.blendModePassThrough },
|
||||
{ value: 'NORMAL', label: panels.value.blendModeNormal },
|
||||
{ value: 'DARKEN', label: panels.value.blendModeDarken },
|
||||
{ value: 'MULTIPLY', label: panels.value.blendModeMultiply },
|
||||
{ value: 'COLOR_BURN', label: panels.value.blendModeColorBurn },
|
||||
{ value: 'LIGHTEN', label: panels.value.blendModeLighten },
|
||||
{ value: 'SCREEN', label: panels.value.blendModeScreen },
|
||||
{ value: 'COLOR_DODGE', label: panels.value.blendModeColorDodge },
|
||||
{ value: 'OVERLAY', label: panels.value.blendModeOverlay },
|
||||
{ value: 'SOFT_LIGHT', label: panels.value.blendModeSoftLight },
|
||||
{ value: 'HARD_LIGHT', label: panels.value.blendModeHardLight },
|
||||
{ value: 'DIFFERENCE', label: panels.value.blendModeDifference },
|
||||
{ value: 'EXCLUSION', label: panels.value.blendModeExclusion },
|
||||
{ value: 'HUE', label: panels.value.blendModeHue },
|
||||
{ value: 'SATURATION', label: panels.value.blendModeSaturation },
|
||||
{ value: 'COLOR', label: panels.value.blendModeColor },
|
||||
{ value: 'LUMINOSITY', label: panels.value.blendModeLuminosity }
|
||||
])
|
||||
const baseBlendModeOptions = useBlendModeOptions(true)
|
||||
|
||||
function blendModeOptions(value: BlendMode | typeof MIXED) {
|
||||
return value === MIXED
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import ColorInput from '@/components/ColorPicker/ColorInput.vue'
|
|||
import NumberField from '@/components/inputs/NumberField.vue'
|
||||
import PropertyItemRow from '@/components/properties/item-list/PropertyItemRow.vue'
|
||||
import PropertyListRoot from '@/components/properties/PropertyListRoot.vue'
|
||||
import {
|
||||
commitDiscretePropertyListChange,
|
||||
useBlendModeOptions
|
||||
} from '@/components/properties/blend-mode/use'
|
||||
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import FillSwatch from '@/components/ui/FillSwatch.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||
import Tip from '@/components/ui/Tip.vue'
|
||||
|
||||
|
|
@ -16,6 +21,7 @@ import type { Effect, Fill } from '@open-pencil/scene-graph'
|
|||
|
||||
const effectsCtx = useEffectsControls()
|
||||
const { panels } = useI18n()
|
||||
const blendModeOptions = useBlendModeOptions()
|
||||
|
||||
function effectPreview(effect: Effect): Fill {
|
||||
return {
|
||||
|
|
@ -29,7 +35,7 @@ function effectPreview(effect: Effect): Fill {
|
|||
|
||||
<template>
|
||||
<PropertyListRoot
|
||||
v-slot="{ items, isMixed, activeNode, actions }"
|
||||
v-slot="{ items, isMixed, activeNode, flush, actions }"
|
||||
prop-key="effects"
|
||||
:label="panels.effects"
|
||||
>
|
||||
|
|
@ -105,6 +111,19 @@ function effectPreview(effect: Effect): Fill {
|
|||
class="ml-[26px] flex flex-col gap-1.5 py-1.5"
|
||||
data-slot="effect-settings"
|
||||
>
|
||||
<PanelFieldGroup :label="panels.blendMode">
|
||||
<AppSelect
|
||||
:model-value="effect.blendMode ?? 'NORMAL'"
|
||||
:options="blendModeOptions"
|
||||
:label="panels.blendMode"
|
||||
data-property="effect-blend-mode"
|
||||
@update:model-value="
|
||||
commitDiscretePropertyListChange(flush, () =>
|
||||
actions.patch(index, { blendMode: $event as Effect['blendMode'] })
|
||||
)
|
||||
"
|
||||
/>
|
||||
</PanelFieldGroup>
|
||||
<template v-if="effectsCtx.isShadow(effect.type)">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Tip :label="panels.xAxis">
|
||||
|
|
|
|||
|
|
@ -19,10 +19,16 @@ import {
|
|||
} from '@/components/properties/paint/binding'
|
||||
import { fillLabel } from '@/components/properties/fill-label'
|
||||
import { createFillOkhclAdapter } from '@/components/properties/paint/okhcl'
|
||||
import {
|
||||
commitDiscretePropertyListChange,
|
||||
useBlendModeOptions
|
||||
} from '@/components/properties/blend-mode/use'
|
||||
import PropertyListRoot from '@/components/properties/PropertyListRoot.vue'
|
||||
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
|
||||
import VariableBindingPicker from '@/components/properties/binding/VariableBindingPicker.vue'
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||
|
||||
import { colorToHexRaw } from '@open-pencil/core/color'
|
||||
|
|
@ -34,6 +40,7 @@ const fillCtx = useFillControls()
|
|||
const okhcl = useOkHCL()
|
||||
const colorProvider = useColorBindingProvider()
|
||||
const { panels, dialogs } = useI18n()
|
||||
const blendModeOptions = useBlendModeOptions()
|
||||
|
||||
function displayFill(fill: Fill, resolvedColor: Color | undefined): Fill {
|
||||
return fill.type === 'SOLID' && resolvedColor ? { ...fill, color: resolvedColor } : fill
|
||||
|
|
@ -78,74 +85,87 @@ function updateSolidColor(
|
|||
|
||||
<p v-if="isMixed" class="text-[11px] text-muted">{{ panels.mixedFillsHelp }}</p>
|
||||
|
||||
<PropertyItemRow
|
||||
v-for="(fill, index) in items"
|
||||
:key="`${index}:${fill.visible ? 'visible' : 'hidden'}`"
|
||||
prop-key="fills"
|
||||
:index="index"
|
||||
:visibility-label="panels.toggleVisibility"
|
||||
:remove-label="panels.removeFill"
|
||||
>
|
||||
<BindableValueRoot
|
||||
v-slot="binding"
|
||||
:provider="colorProvider"
|
||||
:targets="paintBindingTargets(selectedNodeIds, 'fills', index)"
|
||||
:value="fill.color"
|
||||
batch-label="Change fill color"
|
||||
<div v-for="(fill, index) in items" :key="`${index}:${fill.visible ? 'visible' : 'hidden'}`">
|
||||
<PropertyItemRow
|
||||
prop-key="fills"
|
||||
:index="index"
|
||||
:visibility-label="panels.toggleVisibility"
|
||||
:remove-label="panels.removeFill"
|
||||
>
|
||||
<PaintField
|
||||
:opacity="fill.opacity"
|
||||
:opacity-label="panels.opacity"
|
||||
@update:opacity="actions.patch(index, { opacity: $event })"
|
||||
<BindableValueRoot
|
||||
v-slot="binding"
|
||||
:provider="colorProvider"
|
||||
:targets="paintBindingTargets(selectedNodeIds, 'fills', index)"
|
||||
:value="fill.color"
|
||||
batch-label="Change fill color"
|
||||
>
|
||||
<template #preview>
|
||||
<FillPicker
|
||||
:fill="displayFill(fill, binding.resolvedValue)"
|
||||
:okhcl="createFillOkhclAdapter(okhcl, activeNode, index)"
|
||||
@update="
|
||||
updatePickerFill(binding.actions, flush, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
@open-change="!$event && commitPaintMutation(binding.actions)"
|
||||
@cancel="cancelPaintMutation(binding.actions)"
|
||||
/>
|
||||
</template>
|
||||
<PaintField
|
||||
:opacity="fill.opacity"
|
||||
:opacity-label="panels.opacity"
|
||||
@update:opacity="actions.patch(index, { opacity: $event })"
|
||||
>
|
||||
<template #preview>
|
||||
<FillPicker
|
||||
:fill="displayFill(fill, binding.resolvedValue)"
|
||||
:okhcl="createFillOkhclAdapter(okhcl, activeNode, index)"
|
||||
@update="
|
||||
updatePickerFill(binding.actions, flush, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
@open-change="!$event && commitPaintMutation(binding.actions)"
|
||||
@cancel="cancelPaintMutation(binding.actions)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #value>
|
||||
<PaintValue
|
||||
v-if="fill.type === 'SOLID'"
|
||||
:color="fill.color"
|
||||
:resolved-color="binding.resolvedValue"
|
||||
:variable-name="binding.variable?.name"
|
||||
:label="panels.fill"
|
||||
@update="
|
||||
updateSolidColor(binding.actions, flush, fill, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
/>
|
||||
<span v-else class="min-w-0 flex-1 truncate font-mono text-xs text-surface">
|
||||
{{ fillLabel(fill) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #value>
|
||||
<PaintValue
|
||||
v-if="fill.type === 'SOLID'"
|
||||
:color="fill.color"
|
||||
:resolved-color="binding.resolvedValue"
|
||||
:variable-name="binding.variable?.name"
|
||||
:label="panels.fill"
|
||||
@update="
|
||||
updateSolidColor(binding.actions, flush, fill, $event, (next) =>
|
||||
actions.update(index, next)
|
||||
)
|
||||
"
|
||||
/>
|
||||
<span v-else class="min-w-0 flex-1 truncate font-mono text-xs text-surface">
|
||||
{{ fillLabel(fill) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="fill.type === 'SOLID'" #binding>
|
||||
<VariableBindingPicker
|
||||
:trigger-label="panels.applyVariable"
|
||||
:search-placeholder="dialogs.search"
|
||||
:empty-label="panels.noVariablesFound"
|
||||
:detach-label="panels.detachVariable"
|
||||
:create-label="
|
||||
panels.createColorVariable({ value: `#${colorToHexRaw(fill.color)}` })
|
||||
"
|
||||
:create-name-placeholder="panels.variableName"
|
||||
:create-submit-label="panels.create"
|
||||
/>
|
||||
</template>
|
||||
</PaintField>
|
||||
</BindableValueRoot>
|
||||
</PropertyItemRow>
|
||||
<template v-if="fill.type === 'SOLID'" #binding>
|
||||
<VariableBindingPicker
|
||||
:trigger-label="panels.applyVariable"
|
||||
:search-placeholder="dialogs.search"
|
||||
:empty-label="panels.noVariablesFound"
|
||||
:detach-label="panels.detachVariable"
|
||||
:create-label="
|
||||
panels.createColorVariable({ value: `#${colorToHexRaw(fill.color)}` })
|
||||
"
|
||||
:create-name-placeholder="panels.variableName"
|
||||
:create-submit-label="panels.create"
|
||||
/>
|
||||
</template>
|
||||
</PaintField>
|
||||
</BindableValueRoot>
|
||||
</PropertyItemRow>
|
||||
<PanelFieldGroup :label="panels.blendMode" class="ml-[26px] mt-1.5">
|
||||
<AppSelect
|
||||
:model-value="fill.blendMode ?? 'NORMAL'"
|
||||
:options="blendModeOptions"
|
||||
:label="panels.blendMode"
|
||||
data-property="fill-blend-mode"
|
||||
@update:model-value="
|
||||
commitDiscretePropertyListChange(flush, () =>
|
||||
actions.patch(index, { blendMode: $event as Fill['blendMode'] })
|
||||
)
|
||||
"
|
||||
/>
|
||||
</PanelFieldGroup>
|
||||
</div>
|
||||
</PanelSection>
|
||||
</PropertyListRoot>
|
||||
</template>
|
||||
|
|
|
|||
39
src/components/properties/blend-mode/use.ts
Normal file
39
src/components/properties/blend-mode/use.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import type { BlendMode } from '@open-pencil/scene-graph'
|
||||
import { useI18n } from '@open-pencil/vue'
|
||||
|
||||
export function commitDiscretePropertyListChange(flush: () => void, update: () => void): void {
|
||||
update()
|
||||
flush()
|
||||
}
|
||||
|
||||
export interface BlendModeOption {
|
||||
value: BlendMode
|
||||
label: string
|
||||
}
|
||||
|
||||
export function useBlendModeOptions(includePassThrough = false) {
|
||||
const { panels } = useI18n()
|
||||
return computed<BlendModeOption[]>(() => [
|
||||
...(includePassThrough
|
||||
? [{ value: 'PASS_THROUGH' as const, label: panels.value.blendModePassThrough }]
|
||||
: []),
|
||||
{ value: 'NORMAL', label: panels.value.blendModeNormal },
|
||||
{ value: 'DARKEN', label: panels.value.blendModeDarken },
|
||||
{ value: 'MULTIPLY', label: panels.value.blendModeMultiply },
|
||||
{ value: 'COLOR_BURN', label: panels.value.blendModeColorBurn },
|
||||
{ value: 'LIGHTEN', label: panels.value.blendModeLighten },
|
||||
{ value: 'SCREEN', label: panels.value.blendModeScreen },
|
||||
{ value: 'COLOR_DODGE', label: panels.value.blendModeColorDodge },
|
||||
{ value: 'OVERLAY', label: panels.value.blendModeOverlay },
|
||||
{ value: 'SOFT_LIGHT', label: panels.value.blendModeSoftLight },
|
||||
{ value: 'HARD_LIGHT', label: panels.value.blendModeHardLight },
|
||||
{ value: 'DIFFERENCE', label: panels.value.blendModeDifference },
|
||||
{ value: 'EXCLUSION', label: panels.value.blendModeExclusion },
|
||||
{ value: 'HUE', label: panels.value.blendModeHue },
|
||||
{ value: 'SATURATION', label: panels.value.blendModeSaturation },
|
||||
{ value: 'COLOR', label: panels.value.blendModeColor },
|
||||
{ value: 'LUMINOSITY', label: panels.value.blendModeLuminosity }
|
||||
])
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 23 KiB |
124
tests/e2e/properties/blend-modes.spec.ts
Normal file
124
tests/e2e/properties/blend-modes.spec.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
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
|
||||
let firstId = ''
|
||||
let secondId = ''
|
||||
|
||||
async function chooseBlend(sectionName: string, option: string) {
|
||||
const section = propertySection(page, sectionName)
|
||||
await section.getByRole('combobox', { name: 'Blend mode' }).click()
|
||||
await page.getByRole('option', { name: option, exact: true }).click()
|
||||
await canvas.waitForRender()
|
||||
}
|
||||
|
||||
async function nodeState(id: string) {
|
||||
return page.evaluate((nodeId) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const node = store.graph.getNode(nodeId)
|
||||
return node
|
||||
? {
|
||||
fillBlend: node.fills[0]?.blendMode ?? 'NORMAL',
|
||||
effectBlend: node.effects[0]?.blendMode ?? 'NORMAL',
|
||||
fillStyleId: node.fillStyleId,
|
||||
effectStyleId: node.effectStyleId
|
||||
}
|
||||
: null
|
||||
}, id)
|
||||
}
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
;[firstId, secondId] = await page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const props = {
|
||||
fills: [
|
||||
{
|
||||
type: 'SOLID' as const,
|
||||
color: { r: 0.85, g: 0.2, b: 0.15, a: 1 },
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL' as const
|
||||
}
|
||||
],
|
||||
effects: [
|
||||
{
|
||||
type: 'DROP_SHADOW' as const,
|
||||
color: { r: 0, g: 0, b: 0, a: 0.25 },
|
||||
offset: { x: 0, y: 4 },
|
||||
radius: 8,
|
||||
spread: 0,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL' as const
|
||||
}
|
||||
],
|
||||
fillStyleId: '1:10',
|
||||
effectStyleId: '1:11',
|
||||
width: 160,
|
||||
height: 100
|
||||
}
|
||||
const first = store.graph.createNode('RECTANGLE', store.state.currentPageId, {
|
||||
...structuredClone(props),
|
||||
name: 'Blend first',
|
||||
x: 160,
|
||||
y: 160
|
||||
})
|
||||
const second = store.graph.createNode('RECTANGLE', store.state.currentPageId, {
|
||||
...structuredClone(props),
|
||||
name: 'Blend second',
|
||||
x: 360,
|
||||
y: 160
|
||||
})
|
||||
store.select([first.id])
|
||||
return [first.id, second.id]
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
test('edits fill and effect blend modes with style detachment and undo', async () => {
|
||||
await chooseBlend('Fill', 'Multiply')
|
||||
expect(await nodeState(firstId)).toMatchObject({ fillBlend: 'MULTIPLY', fillStyleId: null })
|
||||
await canvas.pressKey('Meta+z')
|
||||
await canvas.waitForRender()
|
||||
expect(await nodeState(firstId)).toMatchObject({ fillBlend: 'NORMAL', fillStyleId: '1:10' })
|
||||
|
||||
const effects = propertySection(page, 'Effects')
|
||||
await effects.getByRole('button', { name: 'Expand effect settings' }).click()
|
||||
await chooseBlend('Effects', 'Screen')
|
||||
expect(await nodeState(firstId)).toMatchObject({ effectBlend: 'SCREEN', effectStyleId: null })
|
||||
await expect(effects).toHaveScreenshot('effect-blend-mode.png')
|
||||
})
|
||||
|
||||
test('batches fill blend modes across a compatible selection', async () => {
|
||||
await page.evaluate(
|
||||
([first, second]) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.select([first, second])
|
||||
},
|
||||
[firstId, secondId]
|
||||
)
|
||||
await canvas.waitForRender()
|
||||
await chooseBlend('Fill', 'Overlay')
|
||||
expect(await nodeState(firstId)).toMatchObject({ fillBlend: 'OVERLAY' })
|
||||
expect(await nodeState(secondId)).toMatchObject({ fillBlend: 'OVERLAY' })
|
||||
|
||||
await canvas.pressKey('Meta+z')
|
||||
await canvas.waitForRender()
|
||||
expect(await nodeState(firstId)).toMatchObject({ fillBlend: 'NORMAL' })
|
||||
expect(await nodeState(secondId)).toMatchObject({ fillBlend: 'NORMAL' })
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
Loading…
Reference in a new issue