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: