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.compose — Function
compose(trans1, trans2)
trans1 ∘ trans2Take 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).
CoordinateTransformations.Translation — Type
Translation(v) <: AbstractAffineMap
Translation(dx, dy) # 2D
Translation(dx, dy, dz) # 3DConstruct the Translation transformation for translating Cartesian points by an offset v = (dx, dy, ...)
DeviceLayout.Transformations.Reflection — Function
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.
DeviceLayout.Transformations.XReflection — Function
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
-1DeviceLayout.Transformations.YReflection — Function
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
1DeviceLayout.Transformations.Rotation — Function
Rotation(Θ; around_pt=nothing)Construct a rotation about the origin or around_pt. Units accepted (no units ⇒ radians).
DeviceLayout.Transformations.RotationPi — Function
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).
DeviceLayout.Transformations.ScaledIsometry — Type
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:
- If
xrefl(f)istrue, a reflection across thex-axis - Rotation by
rotation(f) - Magnification by
mag(f) - 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.
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.centered — Function
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.
DeviceLayout.magnify — Function
magnify(geom, mag)Returns a copy of geom magnified by a factor of mag.
The origin is the center of magnification.
DeviceLayout.reflect_across_line — Function
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.
DeviceLayout.reflect_across_xaxis — Function
reflect_across_xaxis(geom)Return a copy of geom reflected across the x-axis.
DeviceLayout.rotate — Function
rotate(ent, rot)Return a copy of geom rotated counterclockwise by rot around the origin.
Units are accepted (no units => radians).
DeviceLayout.rotate90 — Function
rotate90(geom, n)Return a copy of geom rotated counterclockwise by n 90° turns.
DeviceLayout.translate — Function
translate(geom, displacement)Return a copy of geom translated by displacement.
Alignment
There are also methods to apply transformations that align objects using the edges of their bounding boxes.
DeviceLayout.Align.above — Function
above(source, target; offset=0, centered=false)Align a copy of source with its bounding box bottom aligned with the top of target's.
DeviceLayout.Align.below — Function
below(source, target; offset=0, centered=false)Align a copy of source with its bounding box top aligned with the bottom of target's.
DeviceLayout.Align.leftof — Function
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.
DeviceLayout.Align.rightof — Function
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.
DeviceLayout.Align.flushbottom — Function
flushbottom(source, target; offset=0, centered=false)Align a copy of source with its bounding box bottom flush with that of target.
DeviceLayout.Align.flushtop — Function
flushtop(source, target; offset=0, centered=false)Align a copy of source with its bounding box top flush with that of target.
DeviceLayout.Align.flushleft — Function
flushleft(source, target; offset=0, centered=false)Align a copy of source with its bounding box left side flush with that of target.
DeviceLayout.Align.flushright — Function
flushright(source, target; offset=0, centered=false)Align a copy of source with its bounding box right side flush with that of target.
DeviceLayout.Align.centered_on — Function
centered_on(source::AbstractGeometry, target::AbstractGeometry)Centers a copy of source centered on the center of target, promoting coordinates if necessary.
DeviceLayout.Align.aligned_to — Function
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))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.
Inspecting transformations
DeviceLayout.Transformations.isapprox_angle — Function
isapprox_angle(α1, α2; atol=1e-9)Test whether angles α1 and α2 are approximately equivalent.
Units may be used for one or both angles (no units => radians).
DeviceLayout.Transformations.isapprox_cardinal — Function
isapprox_cardinal(α; atol=1e-9)Test whether α is approximately a cardinal direction (0°, 90°, 180°, or 270°).
Units may be used. If α has no units, it is treated as an angle in radians.
DeviceLayout.Transformations.mag — Function
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).
mag(ref::GeometryReference)The magnification (uniform scaling factor) applied by transformation(ref).
DeviceLayout.Transformations.origin — Function
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.
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)).
origin(sch::Schematic, node::ComponentNode)
origin(sch::Schematic, node_idx::Int)The origin of node in the global coordinate system of sch.
DeviceLayout.Transformations.preserves_angles — Function
preserves_angles(f::Transformation)Return true if f is angle-preserving (has equal-magnitude eigenvalues) and false otherwise.
Uses approximate equality to allow for floating point imprecision.
DeviceLayout.Transformations.rotated_direction — Function
rotated_direction(angle, trans)Return the new direction that angle maps to under the transformation trans.
DeviceLayout.Transformations.rotation — Function
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.
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).
DeviceLayout.Transformations.rounding_safe — Function
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).
DeviceLayout.Transformations.xrefl — Function
xrefl(f::Transformation)Return true if f applies a reflection (has negative determinant) and false otherwise.
xrefl(ref::GeometryReference)A Bool indicating whether transformation(ref) includes a reflection.
Implementation details
Geometry objects implement specializations of the transform function that determine how they behave under transformations:
DeviceLayout.transform — Function
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
endwhich will throw a DomainError if !preserves_angles(f) (f is not a scaled isometry).
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).