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 — Functioncompose(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 — TypeTranslation(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 — FunctionReflection(α; 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 — FunctionXReflection()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 — FunctionYReflection()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 — FunctionRotation(Θ; around_pt=nothing)Construct a rotation about the origin or around_pt. Units accepted (no units ⇒ radians).
DeviceLayout.Transformations.RotationPi — FunctionRotationPi(Θ_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 — Typestruct 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 — Functioncentered(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 — Functionmagnify(geom, mag)Returns a copy of geom magnified by a factor of mag.
The origin is the center of magnification.
DeviceLayout.reflect_across_line — Functionreflect_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 — Functionreflect_across_xaxis(geom)Return a copy of geom reflected across the x-axis.
DeviceLayout.rotate — Functionrotate(ent, rot)Return a copy of geom rotated counterclockwise by rot around the origin.
Units are accepted (no units => radians).
DeviceLayout.rotate90 — Functionrotate90(geom, n)Return a copy of geom rotated counterclockwise by n 90° turns.
DeviceLayout.translate — Functiontranslate(geom, displacement)Return a copy of geom translated by displacement.
Base.:+ — Method+(ent::AbstractGeometry, p::Point)
+(p::Point, ent::AbstractGeometry)Translate an entity by p.
Base.:- — Method-(ent::AbstractGeometry, p::Point)Translate an entity by -p.
Base.:* — Method*(ent::AbstractGeometry, a::Real)
*(a::Real, ent::AbstractGeometry)Magnify an entity by a.
Base.:/ — Method/(ent::AbstractGeometry, a::Real)Magnify an entity by inv(a).
Alignment
There are also methods to apply transformations that align objects using the edges of their bounding boxes.
DeviceLayout.Align.above — Functionabove(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 — Functionbelow(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 — Functionleftof(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 — Functionrightof(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 — Functionflushbottom(source, target; offset=0, centered=false)Align a copy of source with its bounding box bottom flush with that of target.
DeviceLayout.Align.flushtop — Functionflushtop(source, target; offset=0, centered=false)Align a copy of source with its bounding box top flush with that of target.
DeviceLayout.Align.flushleft — Functionflushleft(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 — Functionflushright(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 — Functioncentered_on(source::AbstractGeometry, target::AbstractGeometry)Centers a copy of source centered on the center of target, promoting coordinates if necessary.
DeviceLayout.Align.aligned_to — Functionaligned_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 — Functionisapprox_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 — Functionisapprox_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 — Functionmag(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 — Functionorigin(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 — Functionpreserves_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 — Functionrotated_direction(angle, trans)Return the new direction that angle maps to under the transformation trans.
DeviceLayout.Transformations.rotation — Functionrotation(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 — Functionrounding_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 — Functionxrefl(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 — Functiontransform(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).