Your question is quite broad, let's have a simple example for 2D shapes.
The code below is defined to that we can write this:
(isomorphism (alexandria:compose (scale 10)
(lift #'round)
(rotate 90))
(triangle (point 0 0)
(point 1 0)
(point 0 1)))
=> (TRIANGLE (POINT 0 0) (POINT 0 10) (POINT -10 0))
This compute a simple transform function known as an isomorphism (which preserves shape), which is first a rotation, then a rounding of the computed points, then a scale operation. The result is a list describing the resulting shape. Copying a shape is simply the isomorphism with function #'identity
(but it is a bit useless if you go with a pure functional way).
NB: the rounding is here e.g. to fall back to zero when cos/sin give very small floats ; the use of floats breaks shape preservation and so does rounding, but here when they are combined the resulting shape is an actual isomorphism. This bit of correctness/accuracy might or might not be important depending of the purpose of your needs. You could also describe what transformations are applied and only "rasterize" them for display.
The transform functions works over a list of coordinates, and return list of coordinates:
(defun translate (dx &optional (dy dx))
(lambda (xy) (mapcar #'+ xy (list dx dy))))
(defun scale (sx &optional (sy sx))
(lambda (xy) (mapcar #'* xy (list sx sy))))
(defun rotate (degrees)
(let* ((radians (* degrees pi 1/180))
(cos (cos radians))
(sin (sin radians)))
(lambda (xy)
(destructuring-bind (x y) xy
(list (- (* x cos) (* y sin))
(+ (* y cos) (* x sin)))))))
(defun lift (fn)
(lambda (things)
(mapcar fn things)))
The isomorphism
function is defined as follows, and recursively destructures shapes into a type-tag (which doubles as a constructor) and components, and in case of points, apply the transform function:
(defun isomorphism (transform shape)
(flet ((isomorphism (s) (isomorphism transform s)))
(with-shape (constructor components) shape
(apply constructor
(if (eq constructor 'point)
(funcall transform components)
(mapcar #'isomorphism components))))))
I defined shape
and with-shape
as follows, to have a little bit of abstraction over how they are represented:
(defun shape (constructor components)
(list* constructor components))
(defmacro with-shape ((constructor components) shape &body body)
`(destructuring-bind (,constructor &rest ,components) ,shape
,@body))
And I can define shapes with simple functions, that may or may not perform some checking and normalization:
(defun point (&rest coords)
(shape 'point coords))
(defun triangle (a b c)
(shape 'triangle (list a b c)))
(defun rectangle (x0 y0 x1 y1)
(shape 'rectangle
(list (min x0 x1)
(min y0 y1)
(max x0 x1)
(max y0 y1))))
Notice how the constructor always is the same symbol as the function's name. This could be enforced with a macro, where you only need return the list of components:
(defconstructor point (x y)
(list x y))
You can also build derived constructors from the one above:
(defun rectangle-xywh (x y width height)
(rectangle x y (+ x width) (+ y height)))
Here above shapes are defined in terms of points, but you can imagine having shapes being assembled from smaller shapes:
(defun group (&rest shapes)
(shape 'group shapes))
This is a bit of a toy example, but that might be useful as a starting point.
Then, if you want to take a shape and make different copies rotated by increments of 90°, you could do:
(loop
for angle from 0 below 360 by 90
collect
(isomorphism (compose (lift #'round)
(rotate angle)
(scale 2)
(translate 10 0))
(group (triangle (point 0 0)
(point 1 0)
(point 0 1)))))