Coordinate Transformations

The mechanism for affine transformations is largely provided by the CoordinateTransformations.jl package. For convenience, the documentation for Translation and compose is reproduced below from that package. We also provide convenience constructors for Reflection across a specified line and Rotation around a specified point, as well as a ScaledIsometry type that represents transformations restricted to those that preserve angles.

Creating transformations

CoordinateTransformations.composeFunction
compose(trans1, trans2)
trans1 ∘ trans2

Take two transformations and create a new transformation that is equivalent to successively applying trans2 to the coordinate, and then trans1. By default will create a ComposedTransformation, however this method can be overloaded for efficiency (e.g. two affine transformations naturally compose to a single affine transformation).

source
CoordinateTransformations.TranslationType
Translation(v) <: AbstractAffineMap
Translation(dx, dy)         # 2D
Translation(dx, dy, dz)     # 3D

Construct the Translation transformation for translating Cartesian points by an offset v = (dx, dy, ...)

source
DeviceLayout.Transformations.ReflectionFunction
Reflection(α; through_pt=nothing)
Reflection(vec::Point; through_pt=nothing)
Reflection(p1::Point, p2::Point)

Construct a reflection across a line.

The line can be specified by two points p1, p2 or by a direction and point through_pt the line passes through. The direction can be a vector or an angle made with the positive x axis (units accepted; no units => radians), and the through_pt is the origin by default.

source
DeviceLayout.Transformations.XReflectionFunction
XReflection()

Construct a reflection about the x-axis (y-coordinate changes sign).

Example:

julia> trans = XReflection()
LinearMap([1 0; 0 -1])

julia> trans(Point(1, 1))
2-element Point{Int64} with indices SOneTo(2):
  1
 -1
source
DeviceLayout.Transformations.YReflectionFunction
YReflection()

Construct a reflection about the y-axis (x-coordinate changes sign).

Example:

julia> trans = YReflection()
LinearMap([-1 0; 0 1])

julia> trans(Point(1, 1))
2-element Point{Int64} with indices SOneTo(2):
 -1
  1
source
DeviceLayout.Transformations.RotationPiFunction
RotationPi(Θ_over_pi=1; around_pt=nothing)

Construct a rotation about the origin or around_pt, with rotation in units of pi (180°).

This may be useful if you know your rotation will be a multiple of 90° but not necessarily which one, since it can be slightly more precise than Rotation (as sincospi is to sincos).

source
DeviceLayout.Transformations.ScaledIsometryType
struct ScaledIsometry{T<:Union{Point, Nothing}} <: AbstractAffineMap
ScaledIsometry(origin=nothing, rotation=0°, xrefl=false, mag=1.0)

A coordinate transformation that preserves angles.

The equivalent transformation of f::ScaledIsometry is the composition of the following transformations, ordered with reflection applied first:

  1. If xrefl(f) is true, a reflection across the x-axis
  2. Rotation by rotation(f)
  3. Magnification by mag(f)
  4. Translation by origin(f)

May be also be constructed as

ScaledIsometry(f::Transformation) = ScaledIsometry(origin(f), rotation(f), xrefl(f), mag(f))

but a DomainError will be thrown if f is not a scaled isometry (does not preserve angles).

Transformation compositions (with compose or ) involving a ScaledIsometry will return a ScaledIsometry if the other transformation also preserves angles.

source

Transformations can also be inverted with inv.

Applying transformations

Coordinate transformations can be applied to any AbstractGeometry object, creating a new object with its coordinates transformed. Transformations created using the constructors in the above section can be applied directly to objects like a function. Here's an example with a Rectangle:

julia> r = Rectangle(2, 1)
Rectangle{Int64}((0,0), (2,1))

julia> trans = Translation(10, 10)
Translation(10, 10)

julia> trans = Rotation(90°) ∘ trans
AffineMap([0.0 -1.0; 1.0 0.0], [-10.0, 10.0])

julia> trans(r)
Rectangle{Float64}((-11.0,10.0), (-10.0,12.0))

Simple transformations

There are methods for conveniently applying simple transformations:

DeviceLayout.centeredFunction
centered(ent::AbstractGeometry; on_pt=zero(Point{T}))

Centers a copy of ent on on_pt, with promoted coordinates if necessary. This function will not throw an InexactError(), even if ent had integer coordinates.

source
DeviceLayout.magnifyFunction
magnify(geom, mag)

Returns a copy of geom magnified by a factor of mag.

The origin is the center of magnification.

source
DeviceLayout.reflect_across_lineFunction
reflect_across_line(geom, dir; through_pt=nothing)
reflect_across_line(geom, p0, p1)

Return a copy of geom reflected across a line.

The line is specified through two points p0 and p1 that it passes through, or by a direction dir (vector or angle made with the x-axis) and a point through_pt that it passes through.

source
DeviceLayout.rotateFunction
rotate(ent, rot)

Return a copy of geom rotated counterclockwise by rot around the origin.

Units are accepted (no units => radians).

source
Base.:+Method
+(ent::AbstractGeometry, p::Point)
+(p::Point, ent::AbstractGeometry)

Translate an entity by p.

source
Base.:-Method
-(ent::AbstractGeometry, p::Point)

Translate an entity by -p.

source
Base.:*Method
*(ent::AbstractGeometry, a::Real)
*(a::Real, ent::AbstractGeometry)

Magnify an entity by a.

source
Base.:/Method
/(ent::AbstractGeometry, a::Real)

Magnify an entity by inv(a).

source

Alignment

There are also methods to apply transformations that align objects using the edges of their bounding boxes.

DeviceLayout.Align.aboveFunction
above(source, target; offset=0, centered=false)

Align a copy of source with its bounding box bottom aligned with the top of target's.

source
DeviceLayout.Align.belowFunction
below(source, target; offset=0, centered=false)

Align a copy of source with its bounding box top aligned with the bottom of target's.

source
DeviceLayout.Align.leftofFunction
leftof(source, target; offset=0, centered=false)

Align a copy of source with its bounding box right side aligned on the left of target's.

source
DeviceLayout.Align.rightofFunction
rightof(source, target; offset=0, centered=false)

Align a copy of source with its bounding box left side aligned on the right of target's.

source
DeviceLayout.Align.flushbottomFunction
flushbottom(source, target; offset=0, centered=false)

Align a copy of source with its bounding box bottom flush with that of target.

source
DeviceLayout.Align.flushtopFunction
flushtop(source, target; offset=0, centered=false)

Align a copy of source with its bounding box top flush with that of target.

source
DeviceLayout.Align.flushleftFunction
flushleft(source, target; offset=0, centered=false)

Align a copy of source with its bounding box left side flush with that of target.

source
DeviceLayout.Align.flushrightFunction
flushright(source, target; offset=0, centered=false)

Align a copy of source with its bounding box right side flush with that of target.

source
DeviceLayout.Align.centered_onFunction
centered_on(source::AbstractGeometry, target::AbstractGeometry)

Centers a copy of source centered on the center of target, promoting coordinates if necessary.

source
DeviceLayout.Align.aligned_toFunction
aligned_to(source::AbstractGeometry{T}, target::AbstractGeometry{S},
           align_source::RectAlignRule, align_target::RectAlignRule;
           offset=convert(S, zero(T))) where {T,S}

Aligns a copy of source with its align_source aligned to align_target of target.

For alignment in only one coordinate, the other coordinate is left unchanged. An optional offset will further displace the result in the aligned coordinate. Coordinates will be promoted if necessary when centering.

align_source and align_target must match coordinates; that is, both must refer to the x coordinate (Align.LeftEdge, Align.RightEdge, or Align.XCenter) or both to the y coordinate (Align.TopEdge, Align.BottomEdge, or Align.YCenter).

Convenience functions (leftof, rightof, above, below, flushleft, flushright, flushtop, flushbottom) are also defined as wrappers around aligned_to with pre-specified AlignRules.

Examples

julia> Align.aligned_to(Rectangle(2, 2), Rectangle(4, 4), Align.LeftEdge(), Align.XCenter())
Rectangle{Float64}((2.0,0.0), (4.0,2.0))
source
aligned_to(source::AbstractGeometry{T}, target::AbstractGeometry{S},
    align_source::Tuple{XAlignRule, YAlignRule},
    align_target::Tuple{XAlignRule, YAlignRule};
    offset::Point = zero(Point{promote_type(S, T)})) where {T,S}

Align a copy of source to target in x and y coordinates simultaneously.

source

Inspecting transformations

DeviceLayout.Transformations.magFunction
mag(f::Translation)

Return the magnification (uniform scaling factor) for f, if it is well defined.

Throws a DomainError if f does not preserve angles ("scaling" depends on direction).

source
mag(ref::GeometryReference)

The magnification (uniform scaling factor) applied by transformation(ref).

source
DeviceLayout.Transformations.originFunction
origin(f::Transformation)

Return the transformed origin if it is translated, or nothing otherwise.

It's necessary to return nothing rather than a zero(Point{T}) if there's no translation, because such transformations (e.g., LinearMaps) may not supply a coordinate type T.

source
origin(ref::GeometryReference)

The origin of the structure that ref points to, in ref's parent coordinate system.

Equivalently, the translation part of transformation(ref) (the transformation that ref would apply to structure(ref)).

source
origin(sch::Schematic, node::ComponentNode)
origin(sch::Schematic, node_idx::Int)

The origin of node in the global coordinate system of sch.

source
DeviceLayout.Transformations.preserves_anglesFunction
preserves_angles(f::Transformation)

Return true if fis angle-preserving (has equal-magnitude eigenvalues) andfalse` otherwise.

Uses approximate equality to allow for floating point imprecision.

source
DeviceLayout.Transformations.rotationFunction
rotation(f::Transformation; α0=0)

Return the change in angle when applying f to a line originally at α0 CCW from the x-axis.

By default, α0 is taken to be 0, and the result is equivalent to the rotation when decomposing the linear part of f into reflection across the x-axis followed by rotation.

Units are accepted for α0 (no units => radians).

If f does not preserve angles, a DomainError is thrown.

source
rotation(ref::GeometryReference; α0=0)

The change in angle when applying transformation(ref) to a line originally at α0 CCW from the x-axis.

Equivalent to rotation(transformation(ref); α0=α0).

source
DeviceLayout.Transformations.rounding_safeFunction
rounding_safe(precision, f::Transformation)

true when applying f gives the same results before or after rounding to precision.

Specifically, if f preserves angles, translates by integer precision, scales by an integer multiplier, and rotates by a multiple of 90°, it is "rounding safe".

precision should either be an integer type like Int32 or unitful type like typeof(1nm).

source
DeviceLayout.Transformations.xreflFunction
xrefl(f::Transformation)

Return true if f applies a reflection (has negative determinant) and false otherwise.

source
xrefl(ref::GeometryReference)

A Bool indicating whether transformation(ref) includes a reflection.

source

Implementation details

Geometry objects implement specializations of the transform function that determine how they behave under transformations:

DeviceLayout.transformFunction
transform(geom::AbstractGeometry, f::Transformation)
transform(
    geom::AbstractGeometry{S};
    origin=zero(Point{S}),
    rot=0°,
    xrefl=false,
    mag=1
)

Return a new AbstractGeometry obtained by applying f to geom.

For generic geom and transformation, attempts to decompose f into a scaled isometry: translate ∘ magnify ∘ rotate ∘ reflect_across_xaxis. In that case, if f does not preserve angles, a DomainError will be thrown.

A concrete subtype of AbstractGeometry must implement

transform(ent::MyEntity, f::Transformation)

It is not, however, required that an arbitrary Transformation be valid on MyEntity. For example, one might write

transform(ent::MyEntity, f::Transformation) = transform(ent, ScaledIsometry(f))
function transform(ent::MyEntity, f::ScaledIsometry)
    # ... create and return transformed entity
end

which will throw a DomainError if !preserves_angles(f) (f is not a scaled isometry).

source

This allows special handling for certain types paired with certain transformations. For example, a Rectangle is by definition axis-aligned. If a transformed Rectangle would still be axis-aligned (for example, the result of a translation or 90° rotation), the result will still be a Rectangle, as in the example above; otherwise, it will be a Polygon.

"Chiral" geometry objects (those that can be either left- or right-handed) can also implement special handling for transformations that include a reflection (which changes handedness).