We distinguish five types of coordinate systems in relation to a sheet:
- local
- coordinates as passed to drawing functions
- sheet
- coordinates are relative to the sheet’s region[fn:3]
- parent
- coordinates are relative to the parent’s region
- native
- coordinates are relative to the mirror’s region
- screen
- 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)))
(compose-transformations
(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)
(compose-transformations
(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.
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 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)
(compose-translation-with-transformation
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?