Boost logo

Boost :

From: Dave Harris (brangdon_at_[hidden])
Date: 2004-12-21 16:20:25


In-Reply-To: <cq90bq$r7v$1_at_[hidden]>
msclrhd_at_[hidden] (Reece Dunn) wrote (abridged):
> > This is because you will almost certainly want transforms that act on
> > arrays of points, and it will sometimes be convenient if they can act
> > on rectangles too.
>
> Translation: a.pos += gui::size( 5, 7 );
> Scaling: a.size += gui::size( 10, 20 );
> Rotation: n/a since you are assuming that each side has an angle of 90
> degrees (allowing for two point representations).

I'd expect to have a family of transform objects, and in particular a
general 2d affine transform represented by a matrix of 6 numbers, along
the lines of:
    x' = x * a + y * b + c;
    y' = x * d + y * e * e;

which can represent scales, shears, rotations, translations or any
combination of them.

Rotations come into play even for axis-aligned rectangles, although they
may be constrained to 90 degree angles. For example, if I want to print a
card that will be folded in half horizontally, the top half must be drawn
upside-down relative to the bottom half. We probably want to treat it as 2
pages, draw both pages right-way-up and side-by-side in the UI, and then
draw the second page upside-down and above the first one to the printer.

This kind of thing is useful for 2D graphics. General affine transforms
are powerful abstraction. This is an area where I have experience; my only
doubt is whether the windowing system can or should use different
conventions to the graphics system. And I think you will eventually want
everything to be consistent and uniform.

> If you have point high, low; then which corner is "high"? Likewise,
> using top_left, bottom_right is bad.

The same issue arises using (point, size). Which point? It could
reasonably be any of the four corners, or the middle.

> Having a (point, size) notation, similar to the Cocoa representation,
> you remove the need for platform dependancy (except when mapping
> between the native type and the independant type).

You still need to know whether:

    r.pos += gui::size( 0, 1 );

moves the rectangle up or down. So you still need to know whether (0,0) is
top-left or bottom-left. You have to pick a convention for axis
orientation and stick with it, and translate where the native model
differs.

The orientation of rectangles drops out of the orientation of the axis.
Actually, you can sometimes allow a rectangle to be specified by any pair
of (diagonally opposite) points, in effect taking their bounding box. This
is what PDF does. Usually, though, you normalise the rectangle so that the
first point contains the smaller values and the other the larger.

Incidently, the conversion from top-left to bottom-left axis orientation
is an example of a general transform. It involves both a scale and a
translate.

> Also, this representation simplifies translation and scaling
> (see above).

Not really. You have some transforms applied to one member and other
transforms to other members. You have to know whether a transform is a
scale or a translate before you can apply it. It's not a regular, uniform
system; not simple.

Also, the code you show for scaling doesn't actually scale. Let's say I
have two rectangles side by side:

    rect a( point(10, 10), size(10, 10) );
    rect b( point(20, 10), size(20, 10) );

I want to scale this to double the size. What do I do?
   
    gui::size scale( 10, 10 );
    a.size += scale;
    b.size += scale;

But that didn't do the job. It made the rectangles overlap, and b's size
didn't double. If we actually want that operation, the (point[2]) code is
just as simple; you increment the second point.

For most operations it makes no difference; neither representation has an
advantage. There are probably operations for which (point,size) is
slightly easier, and there are other operations for which (point[2]) is
easier. I believe (point[2]) is easier for more of the important ones.
Testing for point inclusion, and rectangle union, intersection etc, for
example.

   rect intersection( const rect &lhs, const rect &rhs ) {
       double x0 = max( lhs.point[0].x, rhs.point[0].x );
       double y0 = max( lhs.point[0].y, rhs.point[0].y );
       double x1 = min( lhs.point[1].x, rhs.point[1].x );
       double y1 = min( lhs.point[1].y, rhs.point[1].y );
       return rect( x0, y0, x1, y1 );
   }

versus:

   rect intersection( const rect &lhs, const rect &rhs ) {
       double x0 = max( lhs.pos.x, rhs.pos.x );
       double y0 = max( lhs.pos.y, rhs.pos.y );
       double x1 = min( lhs.pos.x+lhs.size.dx, rhs.pos.x+rhs.size.dx );
       double y1 = min( lhs.pos.y+lhs.size.dy, rhs.pos.y+rhs.size.dy );
       return rect( x0, y0, x1-x0, y1-y0 );
   }

The (point,size) version has 6 extra additions or subtractions.

> On review, I have removed size/size, size*size. As for the
> representation change: this is an evolving library, but the essence
> remains the same, so discussion here is still valid. I do not intend on
> posting the full member function list as these e-mails are long enough
> already! If you are interested, take a look at the code (as you have
> done). I appreciate the feedback.

The big questions are, should the underlying representation be exposed in
the form of public member variables at all? And, does a windowing library
need to pay any attention to the needs of a hypothetical drawing library?

My answers are "no" and "yes".

-- Dave Harris, Nottingham, UK


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk