In JavaFX, each node in the scenegraph can be translated, rotated, scaled or sheared relative to its parent. In mathematical terms, each node maintains a transformation describing its own local coordinate system. This transformation can be defined with a matrix represented as a Transform object in JavaFX.

The JavaFX API offers some convenience methods to manipulate this transformation. E.g., you can scale a node in x-direction and translate it by 42 units in y-direction by calling

node.setScaleX(2); node.setTranslateY(42)Unfortunately, these convenience methods do not accumulate as one would expect. Instead of multiplying the matrices for subsequent transformations, the only set specific entries in the matrix. As a result, translating an object before rotating yields the same result as applying these transformations in reverse order. This is mathematically wrong and does also not match the users expectations.

In my diagram editor application, I want to scroll (translate), zoom (scale) and rotate the canvas using mouse gestures. From the user's point of view it's imperative that each transformation builds on the previous state. The convenience methods don't match this usecase.

The correct solution to this problem is to multiply the transformation matrices. Unfortunately, JavaFX lacks any kind of calculation API for Transform and its subclasses.

Once again, this is where Xtend comes to our aid. An extension method in Xtend can be used to define functions for existing (closed) types, which syntactically look like being methods of the type on the caller's side. Affine is a subtype of Transform that allows its matrix entries to change. So I wrote extension methods to translate, rotate, scale and shear an existing Affine by multiplying the respective transformation matrix, e.g.

class TransformExtensions { def static scale(Affine it, double x, double y) { // left multiply a scale matrix to it // highly optimized as there are many zeros in the scale matrix mxx = x * mxx xy = x * mxy mxz = x * mxz tx = x * tx // take existing translation into account myx = y * myx myy = y * myy myz = y * myz ty = y * ty

} }Importing these as extensions

import static extension ...TransformExtensions.*now allows to accumulate the transformations, e.g.

val diagram = scene.root val diagramTransform = new Affine diagram.transforms.clear diagram.transforms += diagramTransform val EventHandlerscrollHandler = [ diagramTransform.translate(deltaX, deltaY)

] scene.onScrollStarted = scrollHandler scene.onScroll = scrollHandler scene.onScrollFinished = scrollHandler val EventHandler rotateHandler = [ diagramTransform.rotate(angle, sceneX, sceneY) ] scene.onRotationStarted = rotateHandler scene.onRotate = rotateHandler scene.onRotationFinished = rotateHandler ...

The resulting behavior looks like the following screenshot. Note that the mouse position is the pivot for rotations and zoom.

The same mechanism can be used to properly place labels along connections:

## 2 comments:

Hi Jan!

Actually, from an API design perspective, it is always wrong for the ordering in which properties are set to have significance. Because translateX, translateY, rotate, etc are properties, they *must not have order dependence*.

However, there is a way built into Node to do what you want. You just need to add transforms to the "transforms" list on each Node. Just use "node.getTransforms().add(...)" and put whatever transform you want into the list. The order in which the transforms are found in the list is the order in which they are multiplied together.

Cheers

Richard

@Richard: From the API perspective you are right. In my case the parameters are not properties. This is why I call the methods translate() instead of setTranslate().

Unfortunately, I find the documentation quite misleading. It would also be nice to document the apparently fixed order in which the transforms are performed.

In the example I am already using node.getTransforms(), but I keep it a singleton list. Aggregating the transforms in the node.getTransforms() doesn't seem to be an option in my case, as there is will be at least one new transform for each performed gesture, thus easily summing up to a huge number of them the longer the app is in use.

Post a Comment