Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems reseting scene graph in JavaFX

Tags:

javafx

I'm having a problem with a large scene graph rapidly changing.

While responding to an event, when I clear the scene graph (getChildren().clear), I sometimes get this exception:

Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: class javafx.scene.Scene cannot be cast to class javafx.scene.Node (javafx.scene.Scene and javafx.scene.Node are in module javafx.graphics of loader 'app')
    at javafx.graphics/javafx.scene.Scene$MouseHandler.handleNodeRemoval(Scene.java:3709)
    at javafx.graphics/javafx.scene.Scene.generateMouseExited(Scene.java:3581)
    at javafx.graphics/javafx.scene.Parent$3.onProposedChange(Parent.java:593)
    at javafx.base/com.sun.javafx.collections.VetoableListDecorator.clear(VetoableListDecorator.java:294)
    at bit.fxzoomer/bit.fxzoomer.SectorPane12.populateMap(SectorPane12.java:72)
    at bit.fxzoomer/bit.fxzoomer.SectorPane12.lambda$addListeners$1(SectorPane12.java:247)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:106)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
    at javafx.base/javafx.beans.property.ObjectProperty.setValue(ObjectProperty.java:72)
    at bit.fxzoomer/bit.fxzoomer.SectorPane12.setViewport(SectorPane12.java:62)
    at bit.fxzoomer/bit.fxzoomer.App.lambda$start$0(App.java:31)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:106)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
    at javafx.base/javafx.beans.property.ObjectProperty.setValue(ObjectProperty.java:72)
    at bit.fxzoomer/bit.fxzoomer.PanZoomPane.setViewport(PanZoomPane.java:99)
    at bit.fxzoomer/bit.fxzoomer.PanZoomPane.lambda$new$3(PanZoomPane.java:82)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:170)
    at javafx.base/com.sun.javafx.binding.BindingHelperObserver.invalidated(BindingHelperObserver.java:52)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.graphics/javafx.scene.Node$LazyBoundsProperty.invalidate(Node.java:9785)
    at javafx.graphics/javafx.scene.Node$MiscProperties.invalidateBoundsInLocal(Node.java:6876)
    at javafx.graphics/javafx.scene.Node.invalidateBoundsInLocal(Node.java:3469)
    at javafx.graphics/javafx.scene.Node.localBoundsChanged(Node.java:4041)
    at javafx.graphics/javafx.scene.Node.doGeomChanged(Node.java:4028)
    at javafx.graphics/javafx.scene.Node$1.doGeomChanged(Node.java:461)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.geomChangedImpl(NodeHelper.java:184)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.geomChanged(NodeHelper.java:137)
    at javafx.graphics/javafx.scene.Parent.childBoundsChanged(Parent.java:1872)
    at javafx.graphics/javafx.scene.Node.notifyParentOfBoundsChange(Node.java:4099)
    at javafx.graphics/javafx.scene.Node.transformedBoundsChanged(Node.java:4060)
    at javafx.graphics/javafx.scene.Node.doTransformsChanged(Node.java:5003)
    at javafx.graphics/javafx.scene.Node$1.doTransformsChanged(Node.java:444)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.transformsChangedImpl(NodeHelper.java:170)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.transformsChanged(NodeHelper.java:119)
    at javafx.graphics/javafx.scene.transform.Transform.transformChanged(Transform.java:2109)
    at javafx.graphics/javafx.scene.transform.Affine$AffineAtomicChange.end(Affine.java:5778)
    at javafx.graphics/javafx.scene.transform.Affine.appendTranslation(Affine.java:2038)
    at javafx.graphics/javafx.scene.transform.Translate.appendTo(Translate.java:539)
    at javafx.graphics/javafx.scene.transform.Affine.append(Affine.java:1502)
    at bit.fxzoomer/bit.fxzoomer.PanZoomPane.translate(PanZoomPane.java:123)
    at bit.fxzoomer/bit.fxzoomer.PanZoomPane.lambda$new$1(PanZoomPane.java:58)
    at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
    at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
    at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3862)
    at javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1849)
    at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2590)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:409)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:299)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:447)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:412)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:446)
    at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
    at javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
    at javafx.graphics/com.sun.glass.ui.mac.MacView.notifyMouse(MacView.java:127)

It starts with that exception, then I get a cascade of these:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot invoke "javafx.scene.Scene.isDepthBuffer()" because the return value of "javafx.scene.Node.getScene()" is null
    at javafx.graphics/com.sun.javafx.scene.input.PickResultChooser.processOffer(PickResultChooser.java:185)
    at javafx.graphics/com.sun.javafx.scene.input.PickResultChooser.offer(PickResultChooser.java:143)
    at javafx.graphics/javafx.scene.Node.doComputeIntersects(Node.java:5263)
    at javafx.graphics/javafx.scene.Node$1.doComputeIntersects(Node.java:456)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.computeIntersectsImpl(NodeHelper.java:180)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.computeIntersects(NodeHelper.java:133)
    at javafx.graphics/javafx.scene.Node.intersects(Node.java:5234)
    at javafx.graphics/javafx.scene.Node.doPickNodeLocal(Node.java:5171)
    at javafx.graphics/javafx.scene.Node$1.doPickNodeLocal(Node.java:450)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocalImpl(NodeHelper.java:175)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocal(NodeHelper.java:128)
    at javafx.graphics/javafx.scene.Node.pickNode(Node.java:5203)
    at javafx.graphics/javafx.scene.Parent.pickChildrenNode(Parent.java:805)
    at javafx.graphics/javafx.scene.Parent$1.pickChildrenNode(Parent.java:136)
    at javafx.graphics/com.sun.javafx.scene.ParentHelper.pickChildrenNode(ParentHelper.java:113)
    at javafx.graphics/javafx.scene.layout.Region.doPickNodeLocal(Region.java:3160)
    at javafx.graphics/javafx.scene.layout.Region$1.doPickNodeLocal(Region.java:184)
    at javafx.graphics/com.sun.javafx.scene.layout.RegionHelper.pickNodeLocalImpl(RegionHelper.java:104)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocal(NodeHelper.java:128)
    at javafx.graphics/javafx.scene.Node.pickNode(Node.java:5203)
    at javafx.graphics/javafx.scene.Parent.pickChildrenNode(Parent.java:805)
    at javafx.graphics/javafx.scene.Parent$1.pickChildrenNode(Parent.java:136)
    at javafx.graphics/com.sun.javafx.scene.ParentHelper.pickChildrenNode(ParentHelper.java:113)
    at javafx.graphics/javafx.scene.layout.Region.doPickNodeLocal(Region.java:3160)
    at javafx.graphics/javafx.scene.layout.Region$1.doPickNodeLocal(Region.java:184)
    at javafx.graphics/com.sun.javafx.scene.layout.RegionHelper.pickNodeLocalImpl(RegionHelper.java:104)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocal(NodeHelper.java:128)
    at javafx.graphics/javafx.scene.Node.pickNode(Node.java:5203)
    at javafx.graphics/javafx.scene.Scene$MouseHandler.pickNode(Scene.java:4005)
    at javafx.graphics/javafx.scene.Scene.pick(Scene.java:2029)
    at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3815)
    at javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1849)
    at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2590)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:409)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:299)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:447)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:412)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:446)
    at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
    at javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
    at javafx.graphics/com.sun.glass.ui.mac.MacView.notifyMouse(MacView.java:127)

Specifically, I'm panning the scene graph (see the answer here Get Viewport of translated and scaled node ). Each time the viewport changes, I redo the scene graph.

The scene graph is quite large, 20-30,000 nodes.

When things are busy, when I'm dragging the scene around rapidly, clicking a lot, infrequently, I will get those exceptions.

This is single threaded, so I don't think I'm seeing a synchronization problem, but perhaps I'm fighting the redisplay thread. Clearly something is happening at the wrong time.

Sometimes it will recover from these errors (it makes a lot of noise of no consequence), other times it seems to wreck the scene and nothing works anymore.

I have tried delaying the update via a timer that gets continually refreshed while dragging, firing only after the dragging has stopped (via Platform.runLater) but even that can fire this exception. I put it all back on the main thread to rule out a synchronization issue.

It's very sporadic. I don't know what's fighting what.

What's causing these, and how can I prevent it?

EDIT: This is a self contained example of it failing. It doesn't even have a large scene graph. Simply run the program, grab the lower left corner and resize it back and forth, and it fails.

Tested with JDK 11 and JFX 17.0.2.

I tried a large scene graph (25,000 elements), tied simply to a resize event, and could not get it to fail. This fails readily.

package bit.fxtest2;

import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;

public class TransformTest9 extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {

        BigGridPane gridPane = new BigGridPane();
        PanZoomPane pzPane = new PanZoomPane(gridPane);
        pzPane.getViewportProperty().addListener((ov, t, t1) -> {
            gridPane.setViewPort(t1);
        });

        Scene scene = new Scene(pzPane, 250, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    class BigGridPane extends Region {

        ObjectProperty<Bounds> viewPortProperty = new SimpleObjectProperty<>();

        public BigGridPane() {
            viewPortProperty.addListener((ov, t, t1) -> {
                populate();
            });
        }

        public void setViewPort(Bounds viewPort) {
            viewPortProperty.setValue(viewPort);
        }

        public void populate() {
            ObservableList<Node> children = getChildren();
            children.clear();
            for (int i = 0; i < 20; i++) {
                for (int j = 0; j < 20; j++) {
                    Rectangle r = new Rectangle(i * 20, j * 20, 20, 20);
                    r.setFill(Color.WHITE);
                    r.setStroke(Color.BLACK);
                    children.add(r);
                }
            }
            System.out.println(children.size());
        }
    }

    class PanZoomPane extends Region {

        private final Node content;

        private final Rectangle clip;

        private Affine transform;

        private Point2D mouseDown;

        private static final double SCALE = 1.01; // zoom factor per pixel scrolled

        Binding<Bounds> viewportBinding;
        ObjectProperty<Bounds> viewportProperty = new SimpleObjectProperty<>();

        public PanZoomPane(Node content) {
            Background background = new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY));
            setBackground(background);
            this.content = content;
            getChildren().add(content);
            clip = new Rectangle();
            setClip(clip);
            transform = Affine.affine(1, 0, 0, 1, 0, 0);
            content.getTransforms().setAll(transform);

            content.setOnMousePressed(event -> mouseDown = new Point2D(event.getX(), event.getY()));
            content.setOnMouseDragged(event -> {
                double deltaX = event.getX() - mouseDown.getX();
                double deltaY = event.getY() - mouseDown.getY();
                translate(deltaX, deltaY);
            });
            content.setOnScroll(event -> {
                double pivotX = event.getX();
                double pivotY = event.getY();
                double scale = Math.pow(SCALE, event.getDeltaY());
                scale(pivotX, pivotY, scale);
            });

            viewportBinding = new ObjectBinding<>() {
                {
                    bind(
                            localToSceneTransformProperty(),
                            boundsInLocalProperty(),
                            content.localToSceneTransformProperty()
                    );
                }

                @Override
                protected Bounds computeValue() {
                    return content.sceneToLocal(localToScene(getBoundsInLocal()));
                }
            };

            viewportBinding.addListener((obs, oldViewport, newViewport) -> setViewport(newViewport));

        }

        public ObjectProperty<Bounds> getViewportProperty() {
            return viewportProperty;
        }

        public void setViewportProperty(ObjectProperty<Bounds> viewportProperty) {
            this.viewportProperty = viewportProperty;
        }

        public Bounds getViewport() {
            return viewportProperty.getValue();
        }

        public void setViewport(Bounds bounds) {
            viewportProperty.setValue(bounds);
        }

        public Node getContent() {
            return content;
        }

        @Override
        protected void layoutChildren() {
            clip.setWidth(getWidth());
            clip.setHeight(getHeight());
        }

        public void scale(double pivotX, double pivotY, double scale) {
            Affine t = transform.clone();
            t.append(Transform.scale(scale, scale, pivotX, pivotY));
        }

        public void translate(double x, double y) {
            transform.append(Transform.translate(x, y));
        }
    }
}

Edit:

I think I have a solution. I've posted it below.

like image 521
Will Hartung Avatar asked Sep 19 '25 10:09

Will Hartung


1 Answers

I think this is the answer.

EDIT: No, it's not. Still having issues. Maybe I'll just destroy and recreate the pane every time -- seems excessive.

@kleopatra mentioned a call to super.layoutChildren in the `PanZoomPane. And while that worked for the test, it did not work for my code.

@jewelsea pointed me to this post and it has this important assumption:

On the third hand, we expect people to set up scene graphs at initialization time and modify nodes in-place, instead of doing scene graph surgery.

This is clearly not what I'm doing. I'm not only doing scene graph surgery, arguably its outright butchering.

I've seen mentions before from @James_D about how some work is better done in layoutChildren, rather than event handlers. So maybe layoutChildren is where we should be looking.

When the PanZoomPane.layoutChildren is called, it will, inevitably, call my custom pane's layoutChildren.

When the PanZoomPane is resized, layoutChildren is called. But when it is panned or zoomed, it will not. To force the issue, in the scale and translate method, I tickle height of the pane:

double height = getHeight();
setHeight(height + 1);
setHeight(height);

This is enough to trigger the layoutChildren call. Apparently layoutChildren is where scene graph surgery is acceptable. So, I moved all of my scene graph code to layoutChildren.

Arguably I should move the tickle in to my content pane, rather than force it from the PanZoomPane.

But the heart of it is that I guess scene graph surgery is frowned upon in event handlers. We should just be playing with properties of the existing scene graph.

I don't know if there is a better solution to this, but for the moment, it seems to be working and seems to make a bit of sense.

like image 191
Will Hartung Avatar answered Sep 22 '25 14:09

Will Hartung