Scene Graph: Sysdyn-scenario

From Developer Documents
Jump to navigation Jump to search

In this scenario, diagram consists of nodes and arcs between them. Nodes are text boxes that scale according to the text inside them. Arcs go from the center of one node to another so that it clipped to the edges of the nodes. An arrow is drawn in the head of the arc (at the point where the arc is clipped). The following editing operations are supported

  1. Changing the name of a node
  2. Moving a node. The arcs should be updated during the operation.
  3. Changing the curvature of a arc. This is done by dragging some point on the arc to other point.

See [1]. The operations should be reponsive so an implementation that does not need server for updates during the operations is preferred.

Specific questions:

  • Can nodes refer to each other? For example, can arc be implemented as
public class ArcNode extends G2DNode {
    protected IConnectable tail = null;
    protected IConnectable head = null;
    protected double angle = 0.0;

    @Override
    public void render(Graphics2D g) {
        calculate an arc from tail to head with given angle
        clip the arc to the clip bounds of the tail and the head
        (or check if connectables have been changed and update the arc if necessary)
    }
}

where

public interface IConnectable {
    double getX();
    double getY();
    Rectangle2D getClipBounds();
}

and

public class TextNode extends G2DNode implements IConnectable { ... }

...answer

In theory, nodes could refer to each other. However, after serialization the references wouldn't work, thus the references must be id based:

  public class ArcNode extends G2DNode {
    protected String tailId = null;
    protected String headId = null;
    protected double angle = 0.0;

    @SyncField("tailId")
    public void setTailId(String id) {
        this.tailId = id;
    }
    @SyncField("headId")
    public void setHeadId(String id) {
        this.headId = id;
    }
    @SyncField("angle")
    public void setAngle(double angle) {
        this.angle = angle;
    }
    @Override
    public void render(Graphics2D g) {
        calculate an arc from tail to head with given angle
        clip the arc to the clip bounds of the tail and the head
        (or check if connectables have been changed and update the arc if necessary)
    }
}
  • How derived fields in the nodes are updated when server updates the client-side node?
public class TextNode extends G2DNode implements IConnectable {
    // The properties of the node
    protected double centerX = 0.0;
    protected double centerY = 0.0;
    protected String label = "";

    // Auxiliary values computed in updateDerivedFields
    protected Rectangle2D clipBounds = null;	   
    protected float textX;
    protected float textY;

    private void updateDerivedFields() {
        ...
    }
}

...answer:

The methods in IConnectable should be actually implemented by every G2DNode (Hence, we might combine these interfaces). Notice that you cannot use serialization annotations with private methods.

public class TextNode extends G2DNode implements IConnectable {
    // The properties of the node
    protected double centerX = 0.0;
    protected double centerY = 0.0;
    protected String label = "";

    // Auxiliary values computed in updateDerivedFields
    protected Rectangle2D clipBounds = null;	   
    protected float textX;
    protected float textY;

    @SyncField({"clipBounds, textX, textY"})
    protected void updateDerivedFields() {
        // Update fields, but create clone of clipBounds to enable comparison between old and new value
    }
} 
  • How the operation 3 is implemented in client-side? For example, when an arc node notifies that it is being dragged, it should prevent other nodes from getting any mouse events. It should also have some place to store the state of the current operation so that this kind of transient structures do not have to be stored to the nodes. In the applet, this is implemented as:
   class Mover implements ActionHandler {
       public void handleDrag(double x, double y) {
           angle = Arcs.angleOfArc(tail.getX(), tail.getY(), x, y, head.getX(), head.getY());
       }
       public void handleRelease(double x, double y) {}        
   }

   @Override
   public ActionHandler handlePress(double x, double y) {
       double dx = x-cx;
       double dy = y-cy;
       double dist = dx*dx + dy*dy;
       if(dist < (r+SELECTION_TOLERANCE)*(r+SELECTION_TOLERANCE) &&
           dist > (r-SELECTION_TOLERANCE)*(r-SELECTION_TOLERANCE)) {
           double angle = Arcs.normalizeAngle(Math.atan2(-dy, dx));
           if(Arcs.areClockwiseOrdered(angle0, angle, angle1))
               return new Mover();
       }
       return null;
   }

...answer

public class ArcNode extends G2DNode {
   protected double angle;
   protected boolean drag = false;

   public void handleEvent(AWTEvent event) {
       if(!(event instanceof MouseEvent)) return;
       MouseEvent me = (MouseEvent)event;
       if(me.getID() == MouseEvent.MOUSE_PRESSED) {
           double dx = me.getPoint().getX()-cx;
           double dy = me.getPoint().getY()-cy;
           double dist = dx*dx + dy*dy;
           if(dist < (r+SELECTION_TOLERANCE)*(r+SELECTION_TOLERANCE) &&
               dist > (r-SELECTION_TOLERANCE)*(r-SELECTION_TOLERANCE)) {
               double angle = Arcs.normalizeAngle(Math.atan2(-dy, dx));
               if(Arcs.areClockwiseOrdered(angle0, angle, angle1))
                   drag = true;
           }
           me.consume();
       } else if(me.getID() == MouseEvent.MOUSE_DRAGGED && drag == true) {
           G2DNode tail = findNode(tailId); // NOTE: findMode not implemented yet
           G2DNode head = findNode(headId);
           angle = Arcs.angleOfArc(tail.getX(), tail.getY(), me.getPoint().getX(), me.getPoint().getY(), head.getX(), head.getY());
           me.consume();
       } else if(me.getID() == MouseEvent.MOUSE_RELEASED) {
           drag = false;
       }
   }
}