Skip to content

Instantly share code, notes, and snippets.

What would you like to do?

Sheet Geometry

Coordinate systems

We distinguish five types of coordinate systems in relation to a sheet:

coordinates as passed to drawing functions
coordinates are relative to the sheet’s region[fn:3]
coordinates are relative to the parent’s region
coordinates are relative to the mirror’s region
coordinates are relative to the graft’s region


(medium-transformation       sheet) ; (local -> sheet)
(sheet-transformation        sheet) ; (sheet -> parent)
(sheet-native-transformation sheet) ; (sheet -> mirror)
(sheet-device-transformation sheet) ; (local -> mirror)

Sheets are arranged in a tree hierarchy. To acquire a transformation between two sheets coordinate systems we use a function sheet-delta-transformation, where the second argument must be an ancestor (or nil) of the first sheet.

;; Parent transformation (sheet -> parent)
(sheet-delta-transformation sheet (sheet-parent sheet))
;; Native transformation (sheet -> mirrored-ancestor -> mirror)
(let ((mirrored-ancestor (sheet-mirrored-ancestor sheet)))
   (sheet-native-transformation mirrored-ancestor)
   (sheet-delta-transformation sheet mirrored-ancestor)))
;; Screen transformation (sheet -> graft)
(sheet-delta-transformation sheet nil)

Given the above, the following relation is always true:

(transformation-equal (sheet-device-transformation sheet)
                       (sheet-native-transformation sheet)
                       (medium-transformation sheet)))

Both sheet-transformation and medium-transformation are setf-able. Changing the former invalidates the cached sheet-native-transformation. Moreover move-sheet may change the sheet-transformation.


Every sheet has a region which is an area (for example an intersection between two ellipses[fn:1]). It is accessed with a function sheet-region.

The operators sheet-native-region and ~sheet-device-region~[fn:4] work in a similar way to the transformation operators with one important difference: the region of the parent clips the region of the child. Each region is expressed in its coordinate system (i.e sheet-native-region is expressed in the native coordinate system).

(medium-clipping-region sheet) ; local clip
(sheet-region sheet)           ; sheet clip
(sheet-native-region sheet)    ; intersection of all sheet regions between the sheet and its mirrored ancestor
(sheet-device-region sheet)    ; intersection of the native region and a local clip

sheet-region is setf-able. Moreover resize-sheet may be called on the sheet to change the sheet’s region[fn:2]. The function move-and-resize-sheet modifies both the transformation and the region.

Sheets and mirrors

Each mirror also has a transformation and a region, however they are a subject to certain restrictions:

  • a mirror transformation must always be a translation (or the identity)
  • a mirror region must always be a rectangle starting at the point [0, 0]

Some backends may impose additional restrictions. For example the X11 protocol specifies that the window position is specified as two int16 coordinates and its size as two uint16 values.

When a mirrored sheet has a region that is not a rectangle, then the mirror region is a bounding-rectangle of that sheet.

Sheet geometry modifiers

Sheet transformation and region are changed with:

  • (setf sheet-transformation)
  • (setf sheet-region)

Sheet geometry is also modified with functions resize-sheet, move-sheet and move-and-resize-sheet. The last function is just a composition of the former two. Functions arguments (x, y) and [width, height] are expressed in the sheet’s parent coordinate system.

CLIM spec proposes the following implementations:

(defmethod move-sheet ((sheet basic-sheet) x y)
  (let ((transform (sheet-transformation sheet)))
    (multiple-value-bind (old-x old-y)
        (transform-position transform 0 0)
      (setf (sheet-transformation sheet)
              transform (- x old-x) (- y old-y))))))

(defmethod resize-sheet ((sheet basic-sheet) width height)
  (setf (sheet-region sheet)
        (make-bounding-rectangle 0 0 width height)))

(defmethod move-and-resize-sheet ((sheet basic-sheet) x y width height)
  (move-sheet sheet x y)
  (resize-sheet sheet width height))

Proposed definitions of functions move-sheet and resize-sheet have a problem, because they assume that a sheet is a rectangle [0 0 width height] and that its transformation is a translation.

We could define these functions by operating on the bounding rectangle of the sheet region in the coordinate system of the parent:

(defmethod move-sheet ((sheet basic-sheet) x y)
  (let ((transf (sheet-transformation sheet))
        (region (sheet-region sheet)))
    (multiple-value-bind (old-x old-y)
        (bounding-rectangle-position (transform-region transf region))
      (unless (and (coordinate= old-x x)
                   (coordinate= old-y y))
        (let ((dx (- x old-x))
              (dy (- y old-y)))
          (setf (sheet-transformation sheet)
                (compose-transformation-with-translation transf dx dy)))))))

(defmethod resize-sheet ((sheet basic-sheet) w h)
  (let* ((region (sheet-region sheet))
         (transf (sheet-transformation sheet))
         (region* (transform-region transf region)))
    (with-bounding-rectangle* (x1 y1 x2 y2) region*
      (let ((new-w (max w 0))
            (new-h (max h 0))
            (old-w (- x2 x1))
            (old-h (- y2 y1)))
        (setf (sheet-region sheet)
              (if (some #'zerop (list old-w old-h new-w new-h))
                  ;; - When old-w=0 or old-h=0 we can't compute sx or sy
                  ;; - When new-w=0 or new-h=0 we can't transform the region
                  ;;   because it will be canonicalized to +nowhere+ and the
                  ;;   sheet position will be lost.
                  ;; In both cases we throw in the towel and replace the old
                  ;; region with a bounding rectangle (to preserve a position
                  ;; of the sheet). -- jd 2021-02-24
                  (multiple-value-bind (x1 y1) (bounding-rectangle-position region)
                    (make-bounding-rectangle x1 y1 (+ x1 new-w) (+ y1 new-h)))
                  (let* ((sx (/ new-w old-w))
                         (sy (/ new-h old-h))
                         (transf* (make-scaling-transformation* sx sy x1 y1))
                         (resized-region* (transform-region transf* region*)))
                    (untransform-region transf resized-region*))))))))

Note, that resize-sheet does not affect the sheet-transformation.


[fn:4] Technically speaking the mirrored ancestor sheet region should be clipped by the mirror region, however we stipulate that the mirror is big enough to contain whole mirrored sheet region, thus the following is true:

(let ((mirror (sheet-direct-mirror msheet))
      (region (transform-region (sheet-native-transformation msheet)
                                (sheet-region msheet))))
  (region-equal region
                (region-intersection region (mirror-region mirror))))

[fn:3] The sheet region is also known as a “drawing plane”

[fn:1] Don’t do that though.

[fn:2] It is not clear what shoudl happen when the current region is not a rectangle - replace it with a rectangle or maybe rather scale it so the bounding rectangle has a matching width and height?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment