Affine transforms consist of 6 numbers which are typically represented in a 3x3 matrix where multiplying the transform T by the vector V results in V'.
[V'] = [T][V] or [x'] [m00 m01 m02] [x] [m00x + m01y + m02] [y'] = [m10 m11 m12] [y] = [m10x + m11y + m12] [1 ] [ 0 0 1] [1] [1]The documentation for ZTransformGroup explains affine transforms in more detail. ZTransformGroup has several methods for modifying aspects of the transform, such as scale, rotate, and translate. It is also possible to animate those changes over time with those methods - or to specify the new transform with a standard Java2D AffineTransform.
In Jazz, ZVisualComponent which is used to represent visual elements, do not have the ability to be transformed. Instead, a transform node must be inserted above the node that contains the visual component. The transform applies to the entire subtree of that node. So, if there are several children of that node, a single transform node will apply to all of those children. Note that a single visual component can be reused in several places in the scenegraph. If those places are each transformed differently, a single visual component can appear in multiple places.
There are two equally valid ways of thinking about the effects of transforms. The first (and typical) way of thinking about transforms is that the transform changes the place within the global coordinate system that the affected objects appear at. For instance, if you have a transform node 'transformNode', and you call:
transformNode.translate(50.0f, 0.0f);The result is that the children of 'transformNode' (and their descendants) all get translated 50 units to the right.
Alternatively, you can think of transforms as representing nested coordinate systems. Then, that same call to translate would result in 'transformNode' being associated with a new coordinate system that is shifted 50 units to the right of the global coordinate system. Then, the visual component associated with transformNode and all of its children would appear at their normal places within the new coordinate system, but to find out where those objects are in global coordinates, you would have to transform the local coordinate system to global coordinates.
Jazz has several utility methods for converting between different coordinate systems. By definition, we say that the root of the Jazz scenegraph is in global coordinates. Every other node is in its own local coordinate system. These utility methods (such as ZNode.localToGlobal and ZNode.globalToLocal) convert points and rectangles from one coordinate system to the other.
The key thing to keep in mind here is that transforming internal nodes have the effect of moving objects within the scenegraph while transforming the camera doesn't affect the scenegraph, but instead changes where a particular camera looks onto the world represented by the scenegraph.
To support applications in managing these sets of nodes, Jazz has a utility called an "editor" that manages these "edit groups". Lets start by thinking about a single node that an application knows about and wants to manipulate. The application first decides that it wants to add a transform to that node. It does this by getting the editor for that node, and requesting a transform:
ZTransformGroup transformNode = node.editor().getTransformGroup();This will create a new transform node, and insert it above the node. Editors are careful in managing these extra nodes, and the next time you ask for the transform of that node, it will return the same transform. We use the terminology that the node that we start with is called the "primary node", all the extra nodes that are created by the editor are called "edit nodes", and the collection of primary and edit nodes altogether are called an "edit group". Given any member of an edit group, you can access the primary node with editor.getNode(), and you can access the top-most member of the edit group with editor.getTop().
The editor manages these edit nodes types:
Jazz provides a default editor with ZSceneGraphEditor. An instance of the editor is created whenever node.editor() is called. However, an application can extend or change the definition of th editor by specifying that a different editor should be used. This is done by defining an application-specific editor, and calling the static method ZNode.setEditorFactory(), and specifying a factory that creates that special editor.
Finally, the visual element is responsible for creating itself and for supporting manipulation of itself. We must look at the details of manipulating an object in some detail. This is because Jazz determines when to render an component based on its bounds, and because components are responsible for indicating to Jazz when they have changed through the repaint() and reshape() methods.
Every visual component must maintain a "model", or an internal data structure that represents the object. For a circle, this may be as simple as a center point, radius, and pen color. For other objects, the model may be more complex. Typical components will support methods that modify itself. The circle may have methods, for instance, to modify its pen width or pen color. It is the responsibility of these methods to indicate they have changed by calling repaint() if the object needs repainting, but the bounds have not changed (i.e., see ZCircle.setPenColor below) - or by calling reshape() if their bounds have changed (i.e., see ZCircle.setPenWidth below).
import java.awt.*; import java.awt.geom.*; import edu.umd.cs.jazz.*; import edu.umd.cs.jazz.util.*; public class ZCircle extends ZVisualComponent { // default values for variables static final public Color penColor_DEFAULT = Color.blue; static final public Color fillColor_DEFAULT = Color.yellow; static final public float penWidth_DEFAULT = 5.0f; // member variables protected Color penColor = penColor_DEFAULT; protected Color fillColor = fillColor_DEFAULT; protected float penWidth = penWidth_DEFAULT; protected Ellipse2D.Float circle; /***************************** Constructors **********************************/ // creates circle at (0,0) with radius = 0 public ZCircle () { circle = new Ellipse2D.Float(); reshape(); } // creates circle with center (x,y) and radius = r public ZCircle(float x, float y, float r) { float half = r / 2; circle = new Ellipse2D.Float(x - half, y - half, r, r); reshape(); } // creates circle within bounding box defined by // point (x,y) with width and height public ZCircle(float x, float y, float width, float height) { float smallerDim = (width < height ? width : height); circle = new Ellipse2D.Float(x, y, smallerDim, smallerDim); reshape(); } /*********** Get/Set functions for variables specific to the circle *********/ public float getPenWidth() { return penWidth; } public void setPenWidth(float p) { penWidth = p; reshape(); // Changing the pen width results in the bounds changing } public Color getPenColor() { return penColor; } public void setPenColor(Color c) { penColor = c; repaint(); // Changing the pen color does not result in a bounds change } public Color getFillColor() { return fillColor; } public void setFillColor(Color c) { fillColor = c; repaint(); // Changing the fill color does not result in a bounds change } public Point2D getCenter() { if (circle.getWidth() != circle.getHeight()) { System.out.println("ERROR: This is not a circle"); } double x = circle.getX() + (circle.getWidth() / 2); double y = circle.getY() + (circle.getHeight() / 2); return (new Point2D.Float((float)x, (float)y)); } /******************* paint, computeBounds, pick **************************/ // tell Jazz how to paint yourself public void paint(Graphics2D g2) { g2.setStroke(new BasicStroke(penWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); if (fillColor != null) { g2.setColor(fillColor); g2.fill(circle); } if (penColor != null) { g2.setColor(penColor); g2.draw(circle); } } // tell Jazz how big you are protected void computeBounds() { // expand bounds to accomodate penWidth float p = penWidth; float p2 = 0.5f * penWidth; Rectangle2D rect = circle.getBounds2D(); bounds.setRect((float)(rect.getX() - p2), (float)(rect.getY() - p2), (float)(rect.getWidth() + p), (float)(rect.getHeight() + p)); } // tell Jazz how to pick you // this function does not need to be overloaded // but it is a good idea to do so public boolean pick(Rectangle2D rect, ZSceneGraphPath path) { boolean picked = false; if (fillColor != null) { // If there is a fill color, then pick the inside of the circle picked = circle.contains(rect); } else if (penColor != null) { // Else, if there is a pen color, pick just the perimeter of the circle float px = (float)(rect.getX() + (0.5f * rect.getWidth())); float py = (float)(rect.getY() + (0.5f * rect.getHeight())); Point2D pt = new Point2D.Float(px, py); double distance = pt.distance(getCenter()); double radius = circle.getWidth() / 2; if ((distance >= (radius - penWidth/2)) && (distance <= (radius + penWidth/2))) { picked = true; } } return picked; } }
With this percolation model, applications can write an event handler for a specific node by putting the event handler on that node. Or, it can write an event handler for all nodes by putting the event handler at the root of the tree. Finally, an application can write a different event handler for each camera by putting the event handler on the node that contains the camera.
Jazz has several standard event handlers which can be used directly by applications. They are:
panEventHandler = new ZPanEventHandler(cameraNode); zoomEventHandler = new ZoomEventHandler(cameraNode); panEventHandler.setActive(true); zoomEventHandler.setActive(true);Obviously, it will become necessary to provide your own event handlers for the many things that you will want your application to do. Thus Jazz provides ZEventHandler which is an interface that may be implemented when defining Jazz event handlers. Although the use of ZEventHandler is not required, it is useful because that way a single object can be marked as being an event handler, and it can be passed around, and made active or inactive.
When writing per-node Jazz event handlers, it is important to understand the algorithm that Jazz follows to fire event handlers when events arrive. For each mouse or mouse motion event, Jazz calls ZDrawingSurface.pick which identifies the object that the pointer was over. The path returned by the pick call is used to identify event handlers as follows:
Starting at the end of the path, the first node is found and
import java.awt.*; import java.awt.geom.*; import java.awt.event.*; import edu.umd.cs.jazz.*; import edu.umd.cs.jazz.util.*; import edu.umd.cs.jazz.event.*; import edu.umd.cs.jazz.component.*; public class SquiggleEventHandler implements ZEventHandler, ZMouseListener, ZMouseMotionListener { private boolean active = false; // True when event handlers are attached to a node private ZNode node = null; // The node the event handlers are attached to private ZGroup drawingLayer; // The node under which to add the new squiggle private ZPolyline polyline; // The polyline currently being drawn private Point2D pt; // A reusable point public SquiggleEventHandler(ZGroup drawingLayer, ZNode node) { this.drawingLayer = drawingLayer; this.node = node; pt = new Point2D.Float(); } /** * Specifies whether this event handler is active or not. * @param active True to make this event handler active */ public void setActive(boolean active) { if (this.active && !active) { // Turn off event handlers this.active = false; node.removeMouseListener(this); node.removeMouseMotionListener(this); } else if (!this.active && active) { // Turn on event handlers this.active = true; node.addMouseListener(this); node.addMouseMotionListener(this); } } public boolean isActive() { return active; } public void mousePressed(ZMouseEvent e) { if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == MouseEvent.BUTTON1_MASK) { // Left button only ZSceneGraphPath path = e.getPath(); ZCamera camera = path.getTopCamera(); pt.setLocation(e.getX(), e.getY()); path.screenToGlobal(pt); polyline = new ZPolyline(pt); ZVisualLeaf leaf = new ZVisualLeaf(polyline); polyline.setPenWidth(5.0f); polyline.setPenColor(Color.red); drawingLayer.addChild(leaf); } } public void mouseDragged(ZMouseEvent e) { if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == MouseEvent.BUTTON1_MASK) { // Left button only ZSceneGraphPath path = e.getPath(); pt.setLocation(e.getX(), e.getY()); path.screenToGlobal(pt); polyline.add(pt); } } public void mouseReleased(ZMouseEvent e) { if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == MouseEvent.BUTTON1_MASK) { // Left button only polyline = null; } } /** * Invoked when the mouse enters a component. */ public void mouseEntered(ZMouseEvent e) { } /** * Invoked when the mouse exits a component. */ public void mouseExited(ZMouseEvent e) { } /** * Invoked when the mouse has been clicked on a component. */ public void mouseClicked(ZMouseEvent e) { } /** * Invoked when the mouse button has been moved on a node * (with no buttons no down). */ public void mouseMoved(ZMouseEvent e) { } }
An excellent starting point for learning about threads is to read what Sun has written about Swing and threads. Almost all of their solutions for multi-threaded code work for Jazz as well. Here are links to three major articles. Threads and Swing, Using a SwingWorker Thread, and The Last Word in Swing Threads.
One specific issue raised in these articles that often causes people problems is that the method
public static void main(String[] args)used to start a java program, is called from a thread that is not the primary event dispatch thread. As a result, once events begin accessing the Jazz scenegraph (ie. when the ZCanvas has been made visible), this main thread (as well as any other) should not modify the scenegraph. This includes animation, changing transforms, adding or deleting nodes, etc.
There are a few good reasons where it may be appropriate to run some code in a separate thread, such as with asynchronous animation. The following code will zoom in while the rest of the application is still active, and responds to events. The trick is that scaling the camera is always called in the primary Swing event dispatch thread.
import javax.swing.SwingUtilities; import edu.umd.cs.jazz.*; public class AnimTest { private boolean zooming = false; // True during animated zooming private ZCamera camera = null; // Caller must specify camera animation should occur within public AnimTest(ZCamera camera) { this.camera = camera; } // Start the animation public void startZooming() { zooming = true; zoomOneStep(); } // Stop the animation public void stopZooming() { zooming = false; } // This gets called to zoom one step public void zoomOneStep() { if (zooming) { camera.scale(1.1f, 0.0f, 0.0f); try { // The sleep here is necessary. Otherwise, there won't be // time for the primary event thread to get and respond to // input events. Thread.sleep(20); // If the sleep was interrupted, then cancel the zooming, // so don't do the next zooming step SwingUtilities.invokeLater(new Runnable() { public void run() { AnimTest.this.zoomOneStep(); } }); } catch (InterruptedException e) { zooming = false; } } } }This is a summary of the reasons we chose to build Jazz to run within a single thread:
One Swing widget that requires special attention in Jazz is the JPopupMenu. Unfortunately, this also affects JMenu and JComboBox as these both maintain an instance of JPopupMenu. The fundamental problem with JPopupMenu is that it is always potentially heavyweight (for instance, the popup can extend outside the bounds of its parent window). Consequently, care must be excercised when using a JPopupMenu in Jazz. A JPopupMenu should likely be managed at the level of the ZCanvas rather than at the level of embedded Swing widgets. JMenu and JComboBox should also not be used with ZSwing. Instead, use ZMenu and ZComboBox exactly as you would their Swing counterparts, then add them to a ZSwing.
To address this problem of incompatible file formats, Jazz also supports a custom file format which is more (although not completely) version resistant. It is a more bulky and also text-based file format. We expect that after Jazz stabilizes, we may introduce an XML based file format, and will eliminate this special format. This special file format is directly analogous to Java Serialization. Jazz classes implement the ZSerializable method and has similar readObject and writeObject methods that can be used just like Serializable objects.
Note that one difference between Java Serialization and Jazz ZSerialization is that Serialization follows all internal references, and thus if you try to write out any single node, all the pointers will be followed, and it will end up writing out the entire scenegraph including the cameras. ZSerialization, on the other hand, does not write out "up" pointers, and so if you write out a node with ZSerialization, it will only write out the sub-tree rooted at the node you specify. If you want to accomplish this functionality using Serialization, your best bet is to disconnect that subtree from the rest of the scenegraph with "getParent().removeChild(this);", write it out, and then add the subtree back.
Unlike Java Serialization, Jazz ZSerialization is not completely automated. In order to make a class ZSerializable, the class must implement the ZSerializable. interface, and it must write out and read the slots it cares about. It does not automatically write out non-transient slots like Serialization does. In addition, A ZSerializable must have a public no-arg constructor, or it will generate a ClassCastException when being read back in.
The class hierarchy of the Jazz scenegraph objects.
Nodes are of two basic types: leaves and groups. A leaf node is one that has no children. A group node has children. Some leaves (ZVisualLeaf) and some groups (ZVisualGroup) can render visual components. Other node types do not cause any actual rendering, but can provide functionality in other ways. For instance, a ZTransformGroup modifies the transform for all of its children. Let us look at each node type.