OOF: Finite Element Analysis of Microstructures

OOFCanvas Manual

This is the manual for OOFCanvas 1.1.1.

Please see the Disclaimer and Copyright notice at the bottom of this document.

OOFCanvas is a replacement for libgnomecanvas, designed for use in OOF2, but hopefully useful elsewhere. OOFCanvas is based on Cairo and is compatible with gtk3. It might eventually also be compatible with gtk+2 or gtk4. It can be called from C++, Python2, or Python3.

OOF2 used libgnomecanvas to display and interact with images and meshes. But libgnomecanvas requires gtk+2, and gtk+2 works only with Python3, not Python2, and Python2 is being phased out. In order to upgrade OOF2 to Python3, we need to first upgrade it from gtk+2 to gtk+3, and to do that we need to first replace libgnomecanvas.

The canvas is a drawing area that can display a variety of shapes, including text. It can be scrolled, zoomed, and printed. It can report which shapes are drawn at a mouse click location.

OOFCanvas is not a drop-in replacement for libgnomecanvas. It’s also not a full-fledged gtk widget. It’s a set of classes that does some of what libgnomecanvas did and uses gtk.

All of the code is in C++. Wrappers for Python 2 or 3 are generated by SWIG.

Contents

Installation

Prequisites

Before installing OOFCanvas, install the following programs and libraries. If you’re using a package manager that packages header files in separate developer packages, install the developer packages. Headers aren’t necessary for cmake, SWIG, or pkg-config.

If you want OOFCanvas to display images loaded by the ImageMagick library, optionally install

If you want to try OOFCanvas’s experimental support for numpy and scikit-image images, you will need to also install

but note that OOF2 does not work well if numpy support is enabled in OOFCanvas, at least in OOF2 version 2.3.0

We don’t really know the minimum acceptable version numbers for the prerequisites. The listed versions are the ones that we’ve been able to use and test. It’s quite possible that earlier versions will work as well.

For detailed instructions on installing the prerequisites using package managers on various systems are on the OOFCanvas prerequisites page. Those instructions aren’t included in this file because they may change and aren’t under our control. This file is included in the OOFCanvas distribution and we don’t want to create a new version everytime installation instructions need to be updated.

Installing OOFCanvas

After installing the prerequisites, build and install OOFCanvas by following these steps. Lines beginning with ‘%’ should be typed in a terminal window. Type everything after the initial ‘%’.

  1. Download the latest OOFCanvas tar (.tar.gz) file from

     https://www.ctcms.nist.gov/oof/oofcanvas/
  2. Create a working directory. In your home directory or some other convenient location, enter

    % mkdir oofcanvas
    % cd oofcanvas
  3. Unpack the tar file. If it’s in your Downloads directory, type

     % tar -xf ~/Downloads/oofcanvas-1.1.0.tar.gz

    (changing the version number if necessary). This will create a directory called oofcanvas-1.1.0, or something like that.

  4. Create a build directory.

     % mkdir build
     % cd build
  5. Configure OOFCanvas by running ccmake:

     % ccmake ../oofcanvas-1.1.0
    • Type “c” to start the configuration.

    • Use the up and down arrow keys to move between fields. To change a text field, press return and type a value. You can use the left and right arrow keys to move around within the existing txt. To accept your changes, press return. To discard your changes, press escape. To toggle a boolean (ON/OFF) field, press return. In a field that takes a preset list of values, the left and right arrow keys cycle through the possibilities.

    • Set CMAKE_BUILD_TYPE to Release.

    • Change CMAKE_INSTALL_PREFIX to the location where you want OOFCanvas to be installed. The default value is probably a system directory which you don’t have permission to modify. Setting the prefix to your home directory (~) is a good choice. If you’re installing into an Anaconda enviroment named OOF2, set CMAKE_INSTALL_PREFIX to ~/Anaconda3/envs/OOF2

    • Set OOFCANVAS_PYTHON_API to Python2 or Python3 if you want to generate the Python interface for OOFCanvas. Set it to None if you don’t need Python. Leave it at Python3 if you’re using OOFCanvas with OOF2.

    • If you’re using Python3, set OOFCANVAS_PYTHON3_VERSION to the Python3 version number by using the right and left arrows to flip through the versions. The default value, Latest, tells cmake to use the latest version it finds. Make sure to choose the same version number that you used when installing the OOFCanvas prerequisites.

      If you have just switched OOFCANVAS_PYTHON_API to Python3, you will have to type “c” before OOFCANVAS_PYTHON3_VERSION appears in the list of settings.

    • Set OOFCANVAS_SWIG_VERSION to the version of SWIG that you have.

    • Set OOFCANVAS_USE_IMAGEMAGICK to ON if you want to be able to use the ImageMagick library to load image files into the canvas.

    • If you want to be able to display images contained in NumPy arrays, set OOFCANVAS_USE_NUMPY to ON. You will first need to enable advanced settings by typing “t”. This feature is not currently recommended if you’re using OOFCanvas with OOF2.

    • Type “c” again to re-configure.

    • Type “g” to generate the build scripts.

    See the CMake manual for full instructions on how to use ccmake.

  6. Build OOFCanvas:

     % make
  7. Install OOFCanvas:

    If CMAKE_INSTALL_PREFIX was set to a system directory, type

     % sudo make install

    otherwise type

     % make install

    This will create shared libraries called liboofcanvas*.so or liboofcanvas*.dylib in <prefix>/lib, a directory called oofcanvas in <prefix>/lib/pythonX.Y/site-packages (where X.Y is your python version number), a file called oofcanvas.pc in <prefix>/lib/pkgconfig, and a directory called oofcanvas in <prefix>/include.

  8. When building a program that uses OOFCanvas, use the compiler and linker options provide by pkg-config oofcanvas:

     % c++ `pkg-config --cflags oofcanvas` -c myfile.cpp ...
     % c++ `pkg-config --libs oofcanvas` myfile.o ... -o myapp

    If you’ve installed OOFCanvas in a nonstandard location (such as an Anaconda environment or your home directory), you may have to tell pkg-config where to find it by setting the environment variable PKG_CONFIG_PATH, e.g.

     % export PKG_CONFIG_PATH=<prefix>/lib/pkgconfig

    where <prefix> is the value of CMAKE_INSTALL_PREFIX you used when configuring OOFCanvas.

  9. When running a program that uses the OOFCanvas python interface, you may need to tell python where to find the OOFCanvas modules, e.g

     % export PYTHONPATH=<prefix>/lib/python3.10/site-packages

    if you’re using python 3.10.

Uninstalling OOFCanvas

Go to the build directory and run make uninstall. This deletes all the installed files but unfortunately leaves empty directories behind.

Programming with OOFCanvas

All the classes and functions described here are defined in the C++ OOFCanvas namespace. For simplicity we haven’t included it explicitly in the discussion below.

Header Files

<prefix>/include/oofcanvas/oofcanvas.h declares all OOFCanvas classes, functions, and constants. If your program uses pkg-config oofcanvas to build, then you can use

#include <oofcanvas.h>   // does not include any gtk code

or

#include <oofcanvasgui.h>  // includes the gtk code as well

in C++, or

import oofcanvas
from oofcanvas import oofcanvasgui

in Python. If you don’t include/import the oofcanvasgui components, you can use still use the OffScreenCanvas and save its contents, but can’t display it on the screen.

Initialization

When using OOFCanvas in Python, it must be initialized by calling

init_OOFCanvas(threaded)

before any other calls. Calling init_OOFCanvas more than once is harmless. The threaded argument is a boolean value indicating whether or not OOFCanvas will be used in a multithreaded environment.

Class Overview

In general, you create a Canvas object and add it to your Gtk3 user interface (or an OffScreenCanvas if you don’t have a GUI). The Canvas contains CanvasLayers, and CanvasLayers contain CanvasItems, such as lines, circles, and text. Items have positions, sizes, and colors, among other attributes. The Canvas can be zoomed and scrolled.

Mouse clicks and motions on the canvas can invoke a callback function.

Coordinate Systems

There are two important coordinates systems: user coordinates and pixel coordinates.

Pixel coordinates measure distance in pixels, with x increasing from left to right and y increasing from top to bottom. The origin is at the upper left corner of the Canvas, which may or may not be visible on the screen. In pixel coordinates, a screen pixel is 1.0 x 1.0 units.

Items drawn on the canvas are specified in user coordinates, which may be anything convenient to the user. x goes from left to right on the screen, and y goes from bottom to top. This is not the convention in many graphics libraries, but is standard in math, physics, and other parts of the real world.

The conversion from user to pixel coordinates depends on the size of the canvas and the current zoom factor, and determines the ppu (pixels per unit). Almost all objects in OOFCanvas are specified in user coordinates, so the user does not need to worry about the pixel coordinate system at all. The one exception is that the sizes of some objects can be specified in pixels.

The Canvas Classes

Three kinds of Canvas objects are defined.

The pixel size of a Canvas or PythonCanvas is determined by the Gtk window that it’s part of. The pixel size of an OffScreenCanvas is only computed when it’s saved as an image and the size of the image is given.

The CanvasLayer Class

Drawing is done by creating one or more CanvasLayers and adding CanvasItems to them. Opaque items in higher layers obscure the items in lower layers. A newly created layer is always topmost. CanvasLayers can be shown, hidden, and reordered, making it easy to change what’s visible on the canvas.

CanvasLayers are created by calling OffScreenCanvas::newLayer() and destroyed by calling either CanvasLayer::destroy() or OffScreenCanvas::deleteLayer().

The CanvasItem Classes

Everything drawn on a Canvas is an instance of a CanvasItem subclass. Pointers to CanvasItems are passed to CanvasLayer::addItem. The CanvasLayer will destroy its CanvasItems when appropriate – the user should never destroy them explicitly.

Each CanvasItem has a bunch of parameters that determine its position, shape, color, and transparency. Position parameters are always given in user coordinates. Some parameters, such as line widths, can be given in either user or pixel units.

In C++, CanvasItems can be created either by calling their constructors, or calling the subclass’s static create method. In Python, only the create method is available, which ensures that ownership of the object remains in C++, and that Python’s garbage collector will not delete an object. The arguments to the create method are always the same as the arguments to the constructor.

Details of each CanvasItem subclass are given somewhere below.

The Mouse

The Canvas’s setMouseCallback method installs a mouse event handler, which will be called whenever a mouse button is pressed or released, the mouse is moved, or the scroll wheel is turned.

Call Canvas::setMouseCallback(MouseCallback callback, void *data) to install a mouse event handler. callback will be called whenever a mouse button is pressed, the mouse is moved, or the window is scrolled.

To install a rubberband that will be displayed when the mouse is moving, call Canvas::setRubberBand(RubberBand*) from the callback for the mouse-down event. The various types of RubberBand and details of how to use them are described in the section on the RubberBand class, below. To stop displaying the RubberBand, pass a null pointer (in C++) or None (in Python) to setRubberBand().

OOFCanvas does not handle selection of objects with the mouse, but it does provide the position of a mouse click as part of the data passed to the callback function. Additionally, it is possible to get a list of all CanvasItems at a point with OffScreenCanvas::clickedItems(const Coord&).

Scrolling

A canvas can be scrolled in one of two ways. It can be connected to GtkScrollBars or other widgets elsewhere in the GUI, and it can respond to scroll events generated within the GtkLayout.

To connect to scroll bars, call scrollbar.set_adjustment(adj) (in Python) or gtk_range_set_adjustment(scrollbar, adj) (in C++), where adj is the GtkAdjustment returned by Canvas::getHAdjustment() or Canvas::getVAdjustment().

If the GtkLayout receives a scroll event, the mousehandler is called with event set to scroll. The x and y values are the changes in position, and can be used to modify the adjustments of the scroll bars:

def mouseCB(eventtype, x, y, button, shift, ctrl, data):
    if eventtype == "scroll":
        sx = horizontalScrollBar.get_adjustment().get_value()
        horizontalScrollBar.get_adjustment().set_value(sx + x)
        ...

A Simple Example

In C++

#include "oofcanvas/guicanvas.h" // gui-dependent classes (Canvas, Rubberband)
#include "oofcanvas/oofcanvas.h" // everything else

double ppu; // pixels per unit -- initialize to something sensible
// Create a Canvas
Canvas canvas(ppu);
// Get a pointer to the GtkLayout widget
GtkWidget *widget = canvas.gtk(); 

// Install the canvas in the gui.  For example, if it's going into
// a GtkFrame,
frame.add(widget);

// Create a canvas layer
CanvasLayer *layer = canvas.newLayer("layername");

// Add items to the layer
double x=1., y=2., radius=1.4;
CanvasCircle *circle = new CanvasCircle(x, y, radius); // In user coordinates.
circle->setLineWidthInPixels(1.5); // In pixel units
Color orange(1., 0.7, 0.0, 0.5); // r, g, b, a, all in [0.0, 1.0]
circle.setFillColor(orange);
layer->addItem(circle);

// Add more items if you want
...

// Draw the items to the canvas
canvas.draw();

The equivalent Python is virtually identical

import oofcanvas
from oofcanvas import oofcanvasgui

oofcanvas.init_OOFCanvas(False)

canvas = oofcanvasgui.Canvas(width=300, height=300, ppu=1.0,
                             vexpand=True, hexpand=True)
frame.add(canvas.layout)

layer = oofcanvas.CanvasLayer("layername")

x = 1.
y = 2.
radius = 1.4
circle = oofcanvas.CanvasCircle.create(x, y, radius)
circle.setLineWidthInPixels(1.5)
orange = oofcanvas.Color(1., 0.7, 0.0).opacity(0.5)
circle.setFillColor(orange)
layer.addItem(circle)

canvas.draw()

Calling Canvas::draw doesn’t actually draw anything. Instead, it generates a Gtk event that causes the real drawing method to be called from the Gtk main loop.

Details of the Classes

This section contains detailed information about all of the externally visible classes in OOFCanvas, starting with the utility classes that are used by the rest of the code.

Utility Types

These classes are defined in the OOFCanvas namespace and are used for some arguments and return values by the main OOFCanvas methods.

Coord

Coord is a position in user coordinates, the coordinate system in which CanvasItems are defined.

The Coord class is defined in C++, but not in Python. Methods that return a position to Python simply return a tuple, (x,y). When an OOFCanvas function in Python requires a position argument, any type that can be indexed can be used. That is, if you have a coordinate class called MyCoord, you can do this:

pt = MyCoord(x, y)
circle = oofcanvas.CanvasCircle(pt, 1.0)

as long as pt[0] is x and pt[1] is y. When an OOFCanvas function returns a Coord, it’s really returning a tuple, so you can do this:

pt = MyCoord( * oofcanvas.someFunctionReturningACoord() )

Whenever a C++ function described below returns a Coord, assume that the Python version works as described above.

The Coord constructors are

The components can be accessed via the x and y data members or via indexing. coord.x == coord[0].

Basic arithmetic, assignment, and equality operations are supported. Coord::norm2() returns the square of the L2 norm. Coord::operator*(const Coord&) is the dot product, and cross(const Coord&, const Coord&) is the cross product.

ICoord

An ICoord is a Coord with integer coefficients, used to identify pixels.

Rectangle

The Rectangle class is not the same as the CanvasRectangle, described below. CanvasRectangle is a CanvasItem that can be displayed. Rectangle is just a region of space.

A Rectangle can be constructed in several ways:

Useful methods are

Color

Colors are stored as RGBA values, which are doubles between 0 and 1.

C++ Constructors:

The only Python constructor is

To change the opacity of a Python color, use

which returns a new Color with the given opacity and the same RGB values.

Predefined constants are defined for black, white, red, green, blue, gray, yellow, magenta, and cyan.

Canvas Classes

OffScreenCanvas

OffScreenCanvas is the base class for the other Canvas classes. As the name implies, it can’t be displayed on the screen, but it can be drawn to and the resulting image can be saved to a file.

OffScreenCanvas exists in both C++ and Python. The discussion below uses C++ syntax, but the translation to Python is trivial, except that (a) Coords are handled as discussed above, and (b) the methods that return a std::vector in C++ return a list in Python.

The constructor is

Layer manipulation methods in OffScreenCanvas
Output methods in OffScreenCanvas
Miscellaneous methods in OffScreenCanvas

Canvas (C++)

Canvas is the C++ class that actually draws to the screen. It is derived from OffScreenCanvas, and it creates a GtkLayout when it is constructed. The GtkLayout should be inserted into the application’s GUI.

The Canvas constructor is

Canvas::Canvas(double ppu)

ppu is the initial value to use for the pixels per unit scale factor when the canvas is empty. A new value will be computed if you call Canvas::zoomToFill() after adding some CanvasItems, so the initial ppu is nearly irrelevant.

All of the methods defined in OffScreenCanvas are available in Canvas. In addition, Canvas defines:

Canvas (Python)

This is the Canvas class that available in Python. It is derived from a SWIG generated wrapper around a C++ class called PythonCanvas, which is derived from OffScreenCanvas.

The Python Canvas creates a GtkLayout using Gtk’s Python interface. The Gtk widget can be accessed directly via Canvas.layout.

The constructor is

Canvas(width, height, ppu, **kwargs)

where width and height are the desired size of the GtkLayout, in pixels. ppu is the initial pixels per unit value. Any additional keyword arguments in kwargs are passed to the GtkLayout constructor.

All of the methods available in OffScreenCanvas and in the C++ Canvas are also available in the Python Canvas, so refer to those sections for the details.

In Python, the Canvas methods that set callback functions expect the callbacks to be Python functions, but are otherwise just like the C++ functions:

CanvasLayer

CanvasLayers hold sets of CanvasItems, which are the things that are drawn on the Canvas. Layers may be raised, lowered, shown, and hidden.

Layers should only be created by a Canvas or OffScreenCanvas, using its newLayer() method.

CanvasLayer methods include:

CanvasItem

CanvasItem is the abstract base class for everything that can be drawn on the canvas. Generally you get a pointer to a new CanvasItem, call its methods to set its properties, and pass the pointer to CanvasLayer::addItem().

In C++, always allocate new CanvasItems with new or use the class’s static create method. In Python, always use the create method. The arguments to create are always the same as the arguments to the constructor.

This is incorrect:

CanvasCircle circ(Coord(0.,0.), 1.0);
layer->addItem(&circ);

but this is correct:

CanvasCircle *circ1 = new CanvasCircle(Coord(0.,0.), 1.0);
layer->addItem(circ1);
CanvasCircle *circ2 = CanvasCircle::create(Coord(0.,0.), 1.);
layer->addItem(circ2);

In Python, don’t do this:

circ = oofcanvas.CanvasCircle((0.,0.), 1.)
layer.addItem(circ)

Do this instead:

circ = oofcanvas.CanvasCircle.create((0.,0.), 1.)
layer.addItem(circ)

After an item has been added to a layer, the layer owns it. The item will be deleted when it is removed from the layer or when the layer is deleted.

CanvasItem defines the following method:

Abstract CanvasItem Subclasses

CanvasShape

This is an abstract base class for most other CanvasItem classes. It describes an object that can be drawn with a line, but not necessarily filled. The default line color is black. There is no default line width. If the line width is not set, nothing will be drawn.

CanvasShape defines the following methods:

By default, lines are solid. They can be made dashed by calling one of the following methods:

CanvasFillableShape

This abstract class is derived from CanvasShape and is used for closed shapes that can be filled with a color. It provides one method:

Concrete CanvasItem Subclasses

These are the actual items that can be drawn, in alphabetical order.

CanvasArrowhead

An arrowhead can be placed on a CanvasSegment. The CanvasArrowhead class is not derived from CanvasShape. Its constructor is

The size of the arrowhead is set by either

or

Either setSize() or setSizeInPixels() must be called before an arrowhead can be drawn.

CanvasCircle

Derived from CanvasFillableShape. Its constructor is

The coordinates of the center and the radius are in user units. To specify the radius in pixels, use CanvasDot instead.

CanvasCurve

A CanvasCurve is a set of line segments connected end to end. It is derived from CanvasShape. It is specified by listing the sequence of Coords joined by the segments. Its constructors are

Points can be added to a CanvasCurve via

or

In Python, the argument to addPoints is a list of Coord-like (ie, indexable) objects.

int CanvasCurve::size() returns the number of points in the curve.

CanvasDot

Derived from CanvasFillableShape, a CanvasDot is a circle with a fixed size in pixels. Its line width is also always measured in pixels. The constructor is

CanvasEllipse

Derived from CanvasFillableShape. The constructor is

where c is the center in user coordinates and the components of r are the radii in user units. r[0] is the radius in the x direction before rotation. The rotation angle in degrees is measured counterclockwise.

CanvasImage

CanvasImage can display a PNG file, or if compiled with the ImageMagick library, any file format that ImageMagick can read. It can also use an image already loaded by ImageMagick. To enable ImageMagick, define OOFCANVAS_USE_IMAGEMAGICK when building OOFCanvas.

If OOFCanvas is built with the OOFCANVAS_USE_NUMPY and PYTHON_API options, then it can display image data stored in a NumPy array, such as one created by scikit-image.

The constructor creates an empty image:

where position is the position of the lower left corner of the image in user coordinates.

Confusion Opportunity! There are two kinds of pixels. There are the pixels on your computer screen, and there are the pixels in the CanvasImage. They don’t have to be the same size. A CanvasImage may be displayed at a different scale from its natural size, in which case one CanvasImage pixel will be larger or smaller than one screen pixel.

Since an empty image isn’t very useful, CanvasImage includes some static factory methods for creating CanvasImage objects.

CanvasImage provides the following useful methods:

CanvasPolygon

A CanvasPolygon is a closed CanvasCurve, derived from CanvasFillableShape. It is specified by listing the user coordinates of the corners of the polygon, counterclockwise. Its constructors are

Points must be added to a polygon in order, clockwise. When drawn, the last point will be connected to the first. There is currently no mechanism for inserting points in the middle of the sequence, or for deleting them.

To add points to a polygon, in C++ use either

or

In Python, use

or

CanvasRectangle

Derived from CanvasFillableShape. The constructor is

where the Coords are the user coordinates of any two opposite corners of the rectangle.

CanvasSegment

A single line segment, derived from CanvasShape. The constructor is

The positions are given in user coordinates.

CanvasSegments

CanvasSegments is derived from CanvasShape and draws a set of unconnected line segments all with the same color and width.

The constructors are

To add segments to the object, use

CanvasText

CanvasText displays text at an arbitrary position and orientation. It is derived from CanvasItem. The text is drawn by the Pango library.

The constructor is

where location is the position of the lower left corner of the text, in user coordinates.

CanvasText methods include

RubberBand

Rubberbands are lines drawn on top of the rest of the Canvas to indicate mouse movements while a mouse button is pressed. To use a rubberband, create a RubberBand object and pass it to GUICanvasImpl::setRubberBand(RubberBand*). The rubberband will be redrawn every time the mouse moves until GUICanvasImpl::removeRubberBand() is called.

Notes:

Various subclasses of RubberBand are defined in rubberband.h:

The appearance of the rubberband is controlled by these functions in the RubberBand base class:

Adding new rubberband classes is described in the appendix, below.

Appendix: Debugging Tools

Building OOFCanvas with CMAKE_BUILD_TYPE set to “Debug” enables some features that can help with debugging.

Appendix: Adding New CanvasItem Subclasses

New CanvasItem subclasses can be derived in C++ from CanvasItem, CanvasShape, or CanvasFillableShape. A CanvasShape is a CanvasItem with predefined methods for setting line drawing parameters. A CanvasFillableShape is a CanvasShape with predefined methods for setting a fill color.

Actually, two classes must be written for each new canvas item. One, derived from CanvasItem, contains the parameters describing the item and is visible to the calling program. The other, derived from the CanvasItemImplementation template, contains the Cairo code for actually drawing the item, and is hidden from the calling program.

The template argument for CanvasItemImplementation is the CanvasItem subclass that the template implements. The template is derived from the non-templated CanvasItemImplBase class, which contains all of the methods that don’t explicitly depend on the template parameter. There are also two templated classes derived from CanvasItemImplementation, CanvasShapeImplementation and CanvasFillableShapeImplementation, which are used to implement common items derived from CanvasShape and CanvasFillableShape.

This will be easier to explain with an example, so what follows is an annotation of the CanvasRectangle class and its implementation.

Bounding Boxes

First, though, comes a discussion of bounding boxes. Every item needs to be able to compute its bounding box, which is the smallest rectangle, aligned with the x and y axes, that completely encloses the item in user space. The rectangle is used to make some computations more efficient, to determine how large the bitmap needs to be, and to define what “zoom to fill” means.

If an item contains components with sizes specified in pixels, it will not be possible to compute the bounding box in user coordinates without knowing the current ppu. As a simple example, consider a circle of radius 2 in user space, with a perimeter that is drawn two pixels wide outside of that radius. Each side of the bounding box is 4 user units plus 4 pixels.

This is potentially a problem, since the bounding box is one of the things that determines the ppu in some situations. Instead, items provide their “bare” bounding box, which is what the bounding box would be if the ppu were infinite and the pixel size were zero. In the example above, the bare bounding box is a square of side 4 centered on the circle.

It is possible that an item’s size is given entirely in pixels, which means that its bare bounding box has size zero in both directions. This is fine. The bounding box will be Rectangle(pt,pt) where pt is a Coord at the position of the item.

An item’s bounding box is stored in its implementation, in a public data member Rectangle CanvasItemImplBase::bbox. It’s public, because an implementation is only visible to its particular CanvasItem subclass. If a change to the CanvasItem changes its bounding box, it can simply reset bbox and call CanvasItemImplBase::modified().

The CanvasItem Subclass

canvasrectangle.h contains the declaration

class CanvasRectangle : public CanvasFillableShape  // [1]
{ 
  protected:
    double xmin, ymin, xmax, ymax;                  // [2]
  public:
    CanvasRectangle(const Coord&, const Coord&);    // [3]
    CanvasRectangle(const Coord*, const Coord*);    // [4]
    static CanvasRectangle *create(const Coord *p0, const Coord *p1); [5]
    virtual const std::string &classname() const;   // [6]
    void update(const Coord&, const Coord&);        // [7]
    double getXmin() const { return xmin; }         // [8]
    double getXmax() const { return xmax; }
    double getYmin() const { return ymin; }
    double getYmax() const { return ymax; }
    friend std::ostream &operator<<(std::ostream &, const CanvasRectangle&);
    virtual std::string print() const;              // [9]
};
  1. CanvasRectangle is derived from CanvasFillableShape, but the notes here apply just as well to items derived from CanvasShape or directly from CanvasItem.

  2. These are all of the parameters that define the rectangle. Line thickness, color, and dashes are not included because they’re set in the CanvasShape base class, and fill color is in the CanvasFillableShape base class.

  3. The constructor needs to set the parameters that describe the rectangle, and to create the implementation:

    CanvasRectangle::CanvasRectangle(const Coord &p0, const Coord &p1)
      : CanvasFillableShape(
          new CanvasRectangleImplementation(this, Rectangle(p0, p1))),
       xmin(p0.x), ymin(p0.y),
       xmax(p1.x), ymax(p1.y)
    {}

    The constructor invokes the CanvasFillableShape constructor, whose argument is a pointer to a new implementation. The item owns the implementation and will delete it when it’s done with it. The implementation class will be discussed below.

  4. This form of the constructor, using pointers instead of references for its arguments, is for use by SWIG when generating the Python interface.

  5. The create method just calls one of the constructors, and returns a pointer to the new CanvasRectangle. It will be the only form of the constructor available to Python.

  6. The classname method is used by the templates in pythonexportable.h to allow a generic CanvasItem object returned from C++ to Python to be interpreted as the correct CanvasItem subclass. The method just returns the name of the class:

    const std::string &CanvasRectangle::classname() const {
        static const std::string name("CanvasRectangle");
        return name;
    }
  7. update() is used to change the parameters of the rectangle, and is used when the rectangle is a rubberband, meaning that it will be reconfigured and redrawn repeatedly:

    void CanvasRectangle::update(const Coord &p0, const Coord &p1) {
        xmin = p0.x;
        ymin = p0.y;
        xmax = p1.x;
        ymax = p1.x;
        implementation->bbox = Rectangle(p0, p1);
        modified();
    }

    Because the change has altered the rectangle’s bounding box, the implementation’s bbox is updated, and modified() is called to indicate that the rectangle will need to be re-rendered.

    A CanvasItem that isn’t used in a rubberband doesn’t need to have an update method.

  8. getXmin(), etc, are convenience functions that might be useful to a user but aren’t actually required by OOFCanvas.

  9. The print() method is required, but it’s really only there for debugging. The to_string() function template in utility.h allows print to be defined in terms of operator<<:

    std::string CanvasRectangle::print() const {
         return to_string(*this);
     }

The CanvasItemImplementation Subclass

Just as a CanvasItem subclass can be derived from CanvasItem, CanvasShape, or CanvasFillableShape, its implementation can be derived from CanvasItemImplementation, CanvasShapeImplementation, or CanvasFillableShapeImplementation. These templates are defined in canvasitemimpl.h and canvasshapeimpl.h. The template parameter is the CanvasItem class that the implementation implements. The templates share a non-templated base class, CanvasItemImplBase, which contains all the code that doesn’t depend on the template parameter.

CanvasRectangleImplementation is be declared and defined entirely within the same C++ file that defines CanvasRectangle, because it is accessed only via virtual functions and the pointer that’s stored in the CanvasRectangle. Because CanvasRectangle is derived from CanvasFillableShape, CanvasRectangleImplementation must be derived from CanvasFillableShapeImplementation. So canvasrectangle.C contains this declaration:

class CanvasRectangleImplementation
    : public CanvasFillableShapeImplementation<CanvasRectangle>       // [1]
  {
  public:
    CanvasRectangleImplementation(CanvasRectangle *item,              // [2]
                                  const Rectangle &bb)                // [2]
      : CanvasFillableShapeImplementation<CanvasRectangle>(item, bb)  // [3]
    {}
    virtual void drawItem(Cairo::RefPtr<Cairo::Context>) const;       // [4]
    virtual bool containsPoint(const OSCanvasImpl*, const Coord&) const; // [5]
  };
  1. The template argument is the CanvasItem subclass that this class implements.

  2. The constructor arguments must include the CanvasItem and its bounding box. The bounding box can be an uninitialized Rectangle if it’s not known yet. In this case, the bounding box is known and has been provided by the caller, the CanvasRectangle constructor.

    If necessary, there can be other arguments here, since it is called only by the associated CanvasRectangle constructor.

  3. The CanvasItem and bounding box must be passed to the base class constructor. CanvasItemImplementation and CanvasShapeImplementation work the same way as the CanvasFillableShapeImplementation used here.

  4. drawItem() must be defined. Given a Cairo::Context, it creates a path, and strokes or fills it, using information in the CanvasRectangle, which it can access using its canvasitem pointer. Because of the templating, canvasitem is a pointer to the correct CanvasItem subclass, CanvasRectangle.

    void CanvasRectangleImplementation::drawItem(
                          Cairo::RefPtr<Cairo::Context> ctxt)
       const
     {
       double w = lineWidthInUserUnits(ctxt);
       double halfw = 0.5*w;
       Rectangle r = bbox;
       r.expand(-halfw); // move all edges inward by half the line width
       ctxt->move_to(r.xmin(), r.ymin());
       ctxt->line_to(r.xmax(), r.ymin());
       ctxt->line_to(r.xmax(), r.ymax());
       ctxt->line_to(r.xmin(), r.ymax());
       ctxt->close_path();
       fillAndStroke(ctxt);
     }

    lineWidthInUserUnits() is defined in CanvasShapeImplementation and gets the desired line width from the CanvasRectangle, or rather its CanvasShape base class. fillAndStroke() is defined in CanvasFillableShapeImplementation and gets line, dash, and fill information from CanvasShape and CanvasFillableShape.

    Note that the perimeter is drawn so that the outer edges of the lines are at the nominal bounds of the rectangle. A different kind of CanvasItem might choose to center the lines on the nominal bounds, but in that case it would have to increase the size of the bounding box.

  5. Given a user-space Coord that is known to be within the item’s bounding box, containsPoint() returns true if the Coord is actually within the item. containsPoint() must be defined, although if an item will never be clicked on, defining it to simply return false is legal.

    Here is the definition from CanvasRectangleImplementation:

     bool CanvasRectangleImplementation::containsPoint(
                const OSCanvasImpl *canvas, const Coord &pt)
     const
     {
     double lw = lineWidthInUserUnits(canvas);
     return canvasitem->filled() || 
               (canvasitem->lined() &&
                  (pt.x - bbox.xmin() <= lw || bbox.xmax() - pt.x <= lw ||
                   pt.y - bbox.ymin() <= lw || bbox.ymax() - pt.y <= lw));
     } 

    Because the given point is known to be within the bounding box, and the rectangle fills the bounding box, there’s nothing to compute if the rectangle is filled. If it’s not filled, it’s necessary to compute whether or not the point is on a perimeter segment.

    The first argument is an OSCanvasImpl*, a pointer to the implementation class for OffScreenCanvas, which is needed for conversion between coordinate systems, if the line width was specified in pixels.

pixelExtents

One more function needs to be defined in any CanvasItemImplementation that includes graphical elements whose size is specified in pixels.

void CanvasItemImplBase::pixelExtents(double &left, double &right, double &up, double &down) const;

sets the distance, in pixel units, that the item extends past its bare bounding box, in each of the given directions. (left == -x, right == +x, up == +y, down == -y) The default version sets all four values to zero. Since CanvasRectangleImplementation draws its lines inside the bounding box, it uses the default version. If its perimeters were drawn with their centerlines on the bounding box edges, pixelExtents would set each of the four arguments to half the line width, assuming that the line width was specified in pixels.

When an item contains elements defined in pixel units as well as elements defined is user units, it’s possible that the bounding box constant for large ppu and ppu-dependent for small ppu, with a crossover at some finite non-zero ppu. Such a canvas item should probably be represented as two or more better-behaved canvas items.

The default version of pixelExtents defined in CanvasItemImplBase sets all four extents to zero. The version in CanvasShapeImplementation will work for any item derived from CanvasShape or CanvasFillableShape whose perimeter line segments are given in pixel units, but has no other pixel unit dimensions. (Actually, it only works approximately, but is good enough if the line segments aren’t too thick.)

Other Useful CanvasItem Methods

Appendix: Adding New RubberBand Classes

Rubberbands are derived from the RubberBand class declared in oofcanvas/oofcanvasgui/rubberband.h. For simple examples, see that file and oofcanvas/oofcanvasgui/rubberband.C. Each class needs to redefine three virtual functions:

Appendix: Internal Details

It shouldn’t be necessary to understand this section in order to use OOFCanvas. It’s here to help development.

Class Hierarchies and Encapsulation

Encapsulation is used to separate the implementation details from the user-visible header files. Encapsulation is handled differently for different classes, depending on the complexity of their inheritance structure.

The Rendering Call Sequence

Each CanvasLayer contains a Cairo::ImageSurface which contains a bitmap of what’s been drawn in the layer, a Cairo::Context which controls drawing to surface, and a Rectangle which is the bounding box (in user coordinates) of all of the layer’s CanvasItems.

When a CanvasItem is added to a CanvasLayer, the layer is marked “dirty” and the item is stored in the layer. No drawing is done at this point.

When all items have been added to the layers, calling GUICanvasImpl::draw() generates a draw event on the GtkLayout. This causes GUICanvasImpl::drawHandler() to be called. The argument to drawHandler is the Cairo::Context for drawing to the GtkLayout’s Cairo::Surface.

GUICanvasImpl::drawHandler() begins by computing the horizontal and vertical offsets that will be used to keep the image centered in the gtk window (if the image is smaller than the window) or at the position determined by the scroll bars (if the image is larger than the window).

Next, drawHandler calls Canvas::setTransform(), which computes the matrix that converts from user coordinates to bitmap coordinates within the layer, given the ppu. The GtkLayout is resized if necessary so that it is large enough to accomodate the bounding boxes of all of the layers, plus an optional margin (set by OffScreenCanvas::setMargin()). Note that a layer’s bounding box, in user units, can depend on the ppu if the layer contains items with sizes given in pixels.

What happens next depends on whether or not a rubberband is being drawn. If there is no rubberband, GUICanvasImpl::drawHandler draws the background color and then, for each layer from bottom to top, tells the layer to draw all of its CanvasItems to its own Cairo::ImageSurface (CanvasLayer::render()), and copies the layer’s surface to the GtkLayout’s surface (CanvasLayer::copyToCanvas()) at the position given by the scroll bars. (CanvasLayer::render() only redraws its items if any have changed since the last time they were drawn.)

If there is an active rubberband, on the first call to drawHandler after the mouse button was pressed all of the CanvasLayers other than the rubberband’s layer are rendered to a separate Cairo::ImageSurface called the nonRubberBandBuffer. Then this buffer is copied to the GtkLayout and the rubberband is drawn on top of it. On subsequent calls to drawHandler, the nonRubberBandBuffer is copied and the rubberband is drawn, but the nonRubberBandBuffer is not rebuilt unless the layers have changed.


NIST-developed software is provided by NIST as a public service. You may use, copy and distribute copies of the software in any medium, provided that you keep intact this entire notice. You may improve, modify and create derivative works of the software or any portion of the software, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the software and should note the date and nature of any such change. Please explicitly acknowledge the National Institute of Standards and Technology as the source of the software. To facilitate maintenance we ask that before distributing modified versions of this software, you first contact the authors at oof_manager@list.nist.gov.

NIST-developed software is expressly provided “AS IS.” NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED, IN FACT OR ARISING BY OPERATION OF LAW, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NIST NEITHER REPRESENTS NOR WARRANTS THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT ANY DEFECTS WILL BE CORRECTED. NIST DOES NOT WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE USE OF THE SOFTWARE OR THE RESULTS THEREOF, INCLUDING BUT NOT LIMITED TO THE CORRECTNESS, ACCURACY, RELIABILITY, OR USEFULNESS OF THE SOFTWARE.

You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. The software developed by NIST employees is not subject to copyright protection within the United States.