Skip to content

fix(main-editor): skip replacing a background screenshot when the lis… - #870

Merged
hm21 merged 8 commits into
hm21:stablefrom
saif-ellafi:fix/replace-screenshot-empty-list
Sep 24, 2026
Merged

hm21 merged 8 commits into
hm21:stablefrom
saif-ellafi:fix/replace-screenshot-empty-list

Conversation

@saif-ellafi

@saif-ellafi saif-ellafi commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

Hi, this PR was AI-Assisted. I found the issues, debugged, and tested.

Two call sites still assumed enableBackgroundGeneration was on.

  1. _takeScreenshot(replaceLastScreenshot: true) always called screenshots.removeLast(). Scale-end does that even when scale-start never reserved a screenshot (canvas pan/zoom with a layer still selected). With background generation off, capture() also never appends, so the list is empty and removeLast throws RangeError. Skip the replace when the list is empty. Widget test covers a middle-button pan with a selected layer and background generation off.

  2. Export of a WidgetLayer with no restore params (exportConfigs) rasterizes via capture(), which returned null whenever background generation was off, then did widgetRecords.add(result) on a List<Uint8List>. Eager history screenshots still skip when the flag is off; an explicit widget capture for export still runs, and null is not added.

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

…t is empty

Scale-end always replaces the screenshot reserved at scale-start. A canvas
pan or zoom with a layer still selected never reserved one, and capture()
appends nothing when background generation is off, so removeLast threw.
…ation is off

capture() treated every call as an eager history screenshot. Exporting a
WidgetLayer without exportConfigs then added null to List<Uint8List>.
@saif-ellafi
saif-ellafi force-pushed the fix/replace-screenshot-empty-list branch from 3ce02ad to b34ac52 Compare September 23, 2026 15:09
… scale-end

A canvas pan or zoom, or a drag selection, that ended with layers selected
ran the layer-transform branch: it discarded the screenshot of the previous
history step and never passed the end to the interactive viewer, so
`onEditorZoomScaleEnd` did not fire. Only a gesture that reserved a history
entry at scale-start replaces its screenshot now.
`ImageConverterService.convert` dropped its `format` argument whenever it
fell back to the main thread (isolate generation off, or no worker support)
and always encoded in `ImageGenerationConfigs.outputFormat`. A widget layer
rasterized for export, or a video editor capture, then came out as JPEG
instead of the PNG it asked for and lost its transparency.
`capture()` is the state-history screenshot: it skips when background
generation is off, tags the capture as a history screenshot (which returns
null on web without worker support) and the crop editor passes it a widget
too, so keying the skip on `screenshots == null` was fragile. Export now
calls `captureWidget()`, which always rasterizes, and `capture()` keeps its
original guard. `_convertLayers` takes a `List<Uint8List>` so a null record
no longer type-checks.
A widget layer exported without a record (its rasterization failed) has
no `recordPosition`, so the import read `widgetRecords[-1]` and threw as
soon as any other widget layer had a record.
Every history step that held the same widget layer rasterized it again and
appended another PNG, but the layer reference, and so the import, only
ever points at the first one.
@hm21

hm21 commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Thanks a lot for this PR, @saif-ellafi. Both crashes were real, and your widget test made the first one easy to reproduce against stable.

Here is what changed on top of your commits:

  • Scale-end is now fixed where it starts. A canvas pan or zoom, and also a drag selection, that ended with layers selected went into the layer-transform branch of _onScaleEnd. With background generation on, that quietly threw away the previous history step's screenshot. The interactive viewer also never got its onScaleEnd, so onEditorZoomScaleEnd didn't fire. The screenshot is now replaced only when _onScaleStart actually reserved a history entry. Otherwise the end event goes to the viewer. Your empty-list guard in _takeScreenshot stays as a safety net.
  • Export now uses a dedicated captureWidget() instead of making capture() skip only when screenshots is passed. capture() is the history screenshot. It marks the capture as one, which returns null on web without worker support, and the crop editor also passes it a widget. capture() keeps its original guard. _convertLayers now takes List<Uint8List>, so a null record is a type error.
  • The main thread now keeps the requested format. With isolate generation off, _convertOnMainThread ignored the requested format and encoded in outputFormat (JPEG by default), so widget records lost their transparency. It now uses the requested format. This also fixes the video editor's PNG captures.
  • Importing a widget layer without a record read widgetRecords[-1] whenever another widget layer had a record. This could happen through the null fallback. It's now guarded.
  • Each widget layer is rasterized only once. Before, every history step that held the same widget layer rasterized it again, but the reference only ever points to the first record.
  • The CHANGELOG entries are shorter, and the release is bumped to 14.3.1.

Thanks again!

@hm21
hm21 merged commit 8a68000 into hm21:stable Sep 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants