3

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.

Will Hartung
  • 115,893
  • 19
  • 128
  • 203
  • “The scene graph is quite large, 20-30,000 nodes.” -> don’t have so many nodes in your scene at the same time. JavaFX is built for smaller numbers of nodes. – jewelsea Jan 31 '22 at 01:20
  • What did this do? `at bit.fxzoomer/bit.fxzoomer.SectorPane12.populateMap(SectorPane12.java:72)`. It would appear, from the error message, `javafx.scene.Scene cannot be cast to class javafx.scene.Node`, that you are trying to put a scene in the scene graph. But the scene graph can only contain nodes. – jewelsea Jan 31 '22 at 01:23
  • The [openjfx performance tips](https://wiki.openjdk.java.net/display/OpenJFX/Performance+Tips+and+Tricks) provides some guidelines for scene graph max size, though I think it has a typo. “Embedded maybe 1000 nodes max, Desktop 10x that Probably 200 on embedded, 20,000 on Desktop should be very fast”. I think that last line should read 2,000 on Desktop would be very fast, that would be inline with the 10x statement for desktop vs embedded. More recent hardware should probably have better performance characteristics too. – jewelsea Jan 31 '22 at 01:40
  • What version of JavaFX is it? Is it the latest stable version? Also, what OS? – jewelsea Jan 31 '22 at 01:44
  • 1
    @jewelsea That line is simply "list.clear()" where "list = getChildren()". It's JFX 13 on MacOS Big Sur. – Will Hartung Jan 31 '22 at 02:41
  • Does it replicate with JavaFX 17.0.2? – jewelsea Jan 31 '22 at 03:26
  • 2
    @jewelsea Yes, it fails on 17.0.2. I've added a test case. – Will Hartung Jan 31 '22 at 05:31
  • hmm (not near my IDE, will try later) .. violation of naming pattern only (getViewportProperty should be viewportProperty) or do you really have api to set the _property_? That's unusual .. – kleopatra Jan 31 '22 at 07:01
  • just looking at the code I would suspect the binding wiring: the changeListener on it forces its validation always which might hit some invalid state "in-between" if the sources are not entirely orthogonal – kleopatra Jan 31 '22 at 07:14
  • 1
    can reproduce an error in the example (though the very first is a unsupportedOperation from children.clear in populate, it's probably just another variant of what you are seeing in the real project) - vanishes on adding super.layoutChildren after sizing the clip (forgot whether or not we are supposed to called super or not, just an observation ;) – kleopatra Jan 31 '22 at 10:33
  • There are a couple of old blog posts which may provide some insight into how JavaFX is implemented which may (or may not) help in understanding some background of what is happening. [post 1](https://www.google.com/amp/s/stuartmarks.wordpress.com/2009/10/12/that-infernal-scene-graph-warning-message/amp/), [post 2](https://stuartmarks.wordpress.com/2009/12/04/that-infernal-scene-graph-warning-message-ii/). It notes regarding node auto removal: “Sometimes (especially with bind) it’s difficult or even impossible to remove the node from the old group first.”. – jewelsea Jan 31 '22 at 10:58
  • Why are you cloning the transform in the `scale()` method? That just seems weird. This will create a new `Transform` object that isn’t the one referenced by the node, but (if we take the API literally) has the same set of listeners which will get fired when you append the scale transform. That almost seems designed to try to break things. – James_D Jan 31 '22 at 12:02
  • That said, if I remove the clone call (as well as all the unnecessary re-direction from bindings to properties) I can still reproduce the issue (JDK 17.0.1, JFX 17.0.1 on Mac). This looks like a bug which should be reported. – James_D Jan 31 '22 at 12:55
  • @kleopatra I tried adding super.layoutChildren after the setClip in the PanAndZoom.layoutChildren, and, you're right, it seems to fix the test case. Unfortunately it doesn't translate to my actual code. – Will Hartung Jan 31 '22 at 15:36
  • @James_D I was cloning the transform so I could apply the scale first to see if I was reaching a boundary condition (i.e. too much/too little zoom). I replaced the clone with t.setToTransform(transform) to copy it. It was probably firing too often before. After the removal, it didn't address the problem. – Will Hartung Jan 31 '22 at 15:48
  • 1
    @jewelsea in the second post, there's this statement: "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." Obviously I'm not doing what it expected. I think I have a solution, which is in my edit. – Will Hartung Jan 31 '22 at 16:14

1 Answers1

1

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.

Will Hartung
  • 115,893
  • 19
  • 128
  • 203
  • 1
    If you want to [generate a layout pass](https://stackoverflow.com/questions/26152642/get-the-height-of-a-node-in-javafx-generate-a-layout-pass), you can do that directly by method calls, you don't need to resort to trickery with height changes. \I don't know that that would actually fix your issue, just commenting to make you aware in case you didn't know. Usually, manual generation of a layout pass is not required and when it is, it is usually just to measure the size of a node after css and layout has been applied. – jewelsea Jan 31 '22 at 19:43