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 ∘ 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).
CoordinateTransformations.Translation
— TypeTranslation(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, ...)
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
-1
DeviceLayout.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
1
DeviceLayout.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 AlignRule
s.
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., LinearMap
s) 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
end
which 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).