Version
@visactor/vrender-core 1.1.5 (also present in @visactor/vrender 1.1.5)
Link to Minimal Reproduction
https://gist.github.com/g1f9/1e280122cbdcb20da77d04d57d06f89e
Single self-contained HTML file (loads vrender from unpkg). Save it and open in a browser — it prints the result to the page, no build setup needed.
Steps to Reproduce
- Put a graphic inside a group that carries a transform (a translate is enough — in practice this is any chart, where marks live under a translated region group).
- Give that graphic
globalZIndex so vrender hoists it into the _builtin_interactive layer.
- Render, then inspect the clone's bounds and try to pick the graphic at its real stage coordinates.
const stage = VRender.createStage({ container: 'app', width: 500, height: 400, autoRender: false });
const root = VRender.createGroup({ x: 100, y: 80 }); // parent transform
stage.defaultLayer.add(root);
const marks = VRender.createGroup({});
const covering = VRender.createGroup({});
root.add(marks);
root.add(covering); // same zIndex, visited first when picking
const circle = VRender.createCircle({ x: 150, y: 100, radius: 12, fill: 'red', globalZIndex: 100 });
marks.add(circle);
covering.add(VRender.createRect({ x: 0, y: 0, width: 300, height: 200, fill: 'rgba(80,140,255,.55)' }));
stage.render();
circle.globalAABBBounds // (238, 168) ~ (262, 192) correct
circle.interactiveGraphic.globalAABBBounds // (138, 88) ~ (162, 112) short by exactly (100, 80)
stage.getLayer('_builtin_interactive').AABBBounds // (138, 88) ~ (162, 112)
stage.pick(250, 180) // -> rect, expected circle
Current Behavior
The clone that InteractiveDrawItemInterceptorContribution.beforeSetInteractive puts into the interactive layer's shadow root keeps only globalZIndex / zIndex:
https://github.com/VisActor/VRender/blob/develop/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts
interactiveGraphic = graphic.clone();
graphic.interactiveGraphic = interactiveGraphic;
interactiveGraphic.baseGraphic = graphic;
interactiveGraphic.setAttributes({ globalZIndex: 0, zIndex: graphic.attribute.globalZIndex }, false, { skipUpdateCallback: true });
this.getShadowRoot(interactiveLayer).add(interactiveGraphic);
The clone never receives the transform its original inherits from its ancestors. Drawing is unaffected, because beforeDrawInteractive renders the baseGraphic and re-applies the matrix by hand:
context.setTransformFromMatrix(baseGraphic.parent.globalTransMatrix, true);
baseGraphic.isContainer ? drawContribution.renderGroup(baseGraphic, ...) : drawContribution.renderItem(baseGraphic, drawContext);
But the clone's own geometry is never corrected, so its globalAABBBounds — and therefore the bounds of the shadow root, of _interactive_group, and of the whole _builtin_interactive layer — are off by the ancestor transform.
Picking then reads those bounds. DefaultPickService.pickGroup gates traversal on them:
const insideGroup = group.AABBBounds.containsPoint(newPoint);
if (!insideGroup && !group.stage.camera) return result;
So whenever the real pointer position falls outside the shifted bounds, the entire interactive layer is skipped and the hoisted graphic is unreachable, even though it is painted on top. Graphics that happen to fall inside the shifted box are still picked correctly, which makes this look intermittent — in a chart, points near one edge of the plot area stop responding to hover while the rest work.
Expected Behavior
A graphic hoisted with globalZIndex should stay pickable at the coordinates where it is actually drawn, regardless of any transform on its ancestors.
Possible fix
Give the clone the transform its original inherits, e.g. in beforeSetInteractive:
interactiveGraphic.setAttributes({
globalZIndex: 0,
zIndex: graphic.attribute.globalZIndex,
postMatrix: graphic.parent ? graphic.parent.globalTransMatrix.clone() : undefined,
}, false, { skipUpdateCallback: true });
postMatrix composes outside the local transform (Graphic.doUpdateLocalMatrix), so this reproduces exactly what the ancestor chain would contribute. The reproduction above applies this at runtime and the result flips from rect to circle, with the clone's bounds becoming identical to the original's. Rendering is unchanged since the draw path uses baseGraphic.
Two side notes, in case they help triage:
- The stale bounds also mean the interactive layer computes its dirty region from the wrong box, which we suspect is behind some repaint artifacts we have seen when toggling
globalZIndex on hover.
- We are currently shipping this as a local prototype patch. Happy to send a PR if the approach looks right.
Version
@visactor/vrender-core 1.1.5 (also present in @visactor/vrender 1.1.5)
Link to Minimal Reproduction
https://gist.github.com/g1f9/1e280122cbdcb20da77d04d57d06f89e
Single self-contained HTML file (loads vrender from unpkg). Save it and open in a browser — it prints the result to the page, no build setup needed.
Steps to Reproduce
globalZIndexso vrender hoists it into the_builtin_interactivelayer.Current Behavior
The clone that
InteractiveDrawItemInterceptorContribution.beforeSetInteractiveputs into the interactive layer's shadow root keeps onlyglobalZIndex/zIndex:https://github.com/VisActor/VRender/blob/develop/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts
The clone never receives the transform its original inherits from its ancestors. Drawing is unaffected, because
beforeDrawInteractiverenders thebaseGraphicand re-applies the matrix by hand:But the clone's own geometry is never corrected, so its
globalAABBBounds— and therefore the bounds of the shadow root, of_interactive_group, and of the whole_builtin_interactivelayer — are off by the ancestor transform.Picking then reads those bounds.
DefaultPickService.pickGroupgates traversal on them:So whenever the real pointer position falls outside the shifted bounds, the entire interactive layer is skipped and the hoisted graphic is unreachable, even though it is painted on top. Graphics that happen to fall inside the shifted box are still picked correctly, which makes this look intermittent — in a chart, points near one edge of the plot area stop responding to hover while the rest work.
Expected Behavior
A graphic hoisted with
globalZIndexshould stay pickable at the coordinates where it is actually drawn, regardless of any transform on its ancestors.Possible fix
Give the clone the transform its original inherits, e.g. in
beforeSetInteractive:postMatrixcomposes outside the local transform (Graphic.doUpdateLocalMatrix), so this reproduces exactly what the ancestor chain would contribute. The reproduction above applies this at runtime and the result flips fromrecttocircle, with the clone's bounds becoming identical to the original's. Rendering is unchanged since the draw path usesbaseGraphic.Two side notes, in case they help triage:
globalZIndexon hover.