Skip to content

fix(dia.Paper): give a re-added layer its own view - #3554

Merged
kumilingus merged 1 commit into
clientIO:masterfrom
kumilingus:fix/paper-layer-readd
Oct 21, 2026
Merged

kumilingus merged 1 commit into
clientIO:masterfrom
kumilingus:fix/paper-layer-readd

Conversation

@kumilingus

@kumilingus kumilingus commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

The core part of #3499, split out so it ships as a @joint/core patch on its own.

onGraphLayerRemove() does not remove a layer view right away: it schedules the removal at the layer view's UPDATE_PRIORITY, which is lower than the cells', so the cell views leave the layer before it goes. When a layer with the same id is added again before that update runs, onGraphLayerAdd() finds the old view still registered under the id and returns without creating a view. Then:

  • cells added to the new layer are inserted into the old view, and its deferred removal throws dia.Paper: The layer view is not empty.
  • otherwise the deferred removal takes the only view of the id, and the next cell on the layer throws dia.Paper: Unknown layer view "layer1".
  • either way the view listens to the removed layer model, not the new one (z changes do not re-sort it)

It happens wherever the update is deferred: on an async paper, on a frozen paper, and on a sync paper inside an UPDATE_DELAYING_BATCHES (translate) batch. A sync paper otherwise removes the view at once and never hits it. @joint/react (async paper) hits it when the declared layers drop an id and declare it again within one frame.

graph.addLayer({ id: 'layer1' });
paper.updateViews();
graph.removeLayer('layer1');     // view removal deferred
graph.addLayer({ id: 'layer1' }); // no new view
graph.addCell({ type: 'standard.Rectangle', layer: 'layer1' });
paper.updateViews();              // throws

Fix

onGraphLayerAdd() keeps the existing view only when it belongs to this layer, or when it is a paper layer view (not a graph layer view). Otherwise it is the view of a removed layer waiting for its removal: it is removed now and a view is created for the new layer.

Differences from the version in #3499:

  • No _dumpLayerViewUpdate(). Removing the view sets its mvc.views registry entry to null, and updateViews() already skips a scheduled update whose view is no longer registered. The pending removal is dropped without a new helper.
  • A paper layer view with the same id is kept. The feat(joint-react): add declarative layers with initialLayers, controlled layers, useLayers and useLayer #3499 check (existingLayerView.model === layer) also matches the paper's own layers: graph.addLayer({ id: 'front' }) removed the paper's front layer view. The GRAPH_LAYER_VIEW_MARKER check keeps the previous behavior for that case.

Alternatives considered

  • Cancel the pending removal and rebind the old view to the new layer. Rejected: the re-added layer may be of another type with another view class (<type>View in layerViewNamespace), and a view does not support swapping its model.
  • Free the id in onGraphLayerRemove() and remove the old view by instance at the update. This keeps the old <g> (and the cell views still in it) in the DOM until the update, but it changes hasLayerView() / getLayerView() right after removeLayer(), needs updateView() to remove the view by instance instead of by id, and paper.remove() would have to track views that are no longer in viewsMap. Larger change for the same result.

The trade-off of the chosen fix: cell views moved out of the layer before it was removed sit in the old <g> until their pending insertion runs, so removing the <g> detaches them from the DOM until then. It only applies where the bug does (the update is deferred):

Paper Detached until
async the next update in rAF - usually before the frame is painted, but with more pending updates than batchSize (default 1000) the insertion can land a frame or more later, and the cell disappears in the meantime
frozen unfreeze() / updateViews()
sync, inside a translate batch batch:stop

Code that measures such a cell view in between (getBBox(), getBoundingClientRect()) gets zero sizes. Cells removed from the layer are discarded anyway, and cells added after the re-add go straight into the new view. The end state is covered by a test.

Tests

New Layers › re-adding a layer before its view is removed, run for an async paper and a frozen paper:

  • the re-added layer has a view bound to it, and one layer element
  • a cell added to the re-added layer renders in it
  • the view of the re-added layer follows z changes
  • a cell moved out of the layer before it was removed renders in its new layer
  • a cell moved back to the re-added layer renders in it
  • a layer removed again after the re-add has no view
  • a layer re-added with another type gets a view of that type
  • a layer with the id of a paper layer leaves the paper layer view in place

On master, 12 of the 16 fail (Unknown layer view "layer1" / The layer view is not empty). The last test fails with the #3499 version of the fix and passes on master.

karma:joint: the 16 new tests pass, and the only failures are the same 5 timing tests that also fail locally on master (debounce, basic transition: *). eslint clean.

Motivation and Context

Needed by #3499 (declarative layers in @joint/react).

🤖 Generated with Claude Code

`onGraphLayerRemove()` defers the removal of a layer view to the next
update, so that the cell views leave the layer first. A layer added again
with the same id before that update found the old view still registered
and got no view of its own: its cells were inserted into the old view,
whose deferred removal then threw "The layer view is not empty", or
lost the view and threw "Unknown layer view".

`onGraphLayerAdd()` now removes the old view of a removed layer with
the same id and creates a view for the new layer. A paper layer view
with that id is kept as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kumilingus
kumilingus marked this pull request as ready for review October 21, 2026 16:01
@kumilingus
kumilingus merged commit b038864 into clientIO:master Oct 21, 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