Haiku BAffineTransform: Coordinate Spaces and Composition
Use Haiku BAffineTransform for predictable coordinate conversion, composition, inversion, hit testing, and transform regression tests.
BAffineTransform represents a two-dimensional affine mapping: scale, shear, rotation, reflection, and translation can be combined without introducing perspective. It is useful when a view has model coordinates that should remain independent of window placement, or when input points must be mapped back from rendered space into an object’s local space.
Most transform bugs are not formula errors. They come from an undocumented choice of coordinate space, a mistaken composition order, or an inverse used after a singular scale. Write down the mapping at each boundary: model-local to view-local, view-local to window, or window to screen. Then test known points through the actual Haiku API instead of relying on a diagram alone.
Read the six coefficients
The current public class exposes six coefficients. Its implementation applies them as:
x' = sx * x + shx * y + tx
y' = shy * x + sy * y + ty
The sx and sy terms are the diagonal scale contributions, shx and shy couple one axis into the other, and tx/ty translate. A pure translation changes only the last two values; a pure scale changes the diagonal. Rotation generally changes more than two coefficients, so avoid using a field name as a complete semantic description after transforms have been composed.
Use the named factories and operations for ordinary work rather than writing coefficient assignments by hand:
#include <AffineTransform.h>
#include <Point.h>
BPoint
ExamplePoint()
{
BAffineTransform localToView = B_AFFINE_IDENTITY_TRANSFORM;
localToView.TranslateBy(12.0, 8.0);
localToView.RotateBy(0.25); // radians
return localToView.Apply(BPoint(4.0, 3.0));
}
This demonstrates API use, not a statement that translation then rotation has the same effect as rotation then translation. Composition order is observable: changing call order changes the coordinate system in which an operation acts. For a user-facing transform pipeline, define what each step means and write a unit test for the resulting point. The current API also provides PreTranslateBy, PreRotateBy, Multiply, and PreMultiply; their names signal distinct composition paths, and should not be interchanged by guesswork.
RotateBy() accepts radians, not degrees. Convert degrees at the UI boundary with a documented conversion and keep the stored model unit explicit. Haiku’s view coordinate orientation and local view origin also matter; do not assume a mathematical y-up drawing convention when the view uses screen-style coordinates. Test with an asymmetric point and non-square geometry so flips and axis swaps are visible.
Keep a transform pipeline explicit
Name transforms by direction, for example modelToCanvas and canvasToView, instead of naming both transform. If A maps model to canvas and B maps canvas to view, derive the combined mapping by applying a point sequentially and comparing it with the composed transform. This is safer than relying on an assumed multiplication order. Keep transforms immutable after publication where practical; if a mutable object is updated during drawing or hit testing, the same event can observe a half-updated pipeline.
Transform points in batches when appropriate. The API supports applying a transform to one BPoint, a pointer, or an array plus count. Ensure the array length is correct and that the caller owns the points being mutated. For model data that must remain in original coordinates, copy the points before applying a destructive array operation or map each point into a separate output buffer.
Rectangles require special handling under rotation and shear. Transforming only the top-left and bottom-right points is insufficient because the other corners can become the extrema. Transform all four corners, then compute the enclosing axis-aligned rectangle. If the desired result is the transformed quadrilateral itself, retain the four points or use a path; a BRect cannot represent a rotated rectangle exactly.
Do not keep recomputing a combined transform through ad hoc arithmetic in multiple modules. Have one layer own the coordinate contract and publish either a composed BAffineTransform or a tested conversion function. For UI hit testing, map the input point into object-local space and test the object’s local geometry. This avoids trying to invert every rendered vertex or maintaining a second approximate hit region.
Inversion and singular transforms
The determinant of the linear portion is sx * sy - shx * shy. A zero determinant means the mapping collapses a dimension and has no inverse; values near zero can make inverse results numerically unstable. The current Invert() operation mutates its receiver and does not report a recoverable failure status. ApplyInverse() is likewise not a substitute for checking whether the transform is safely invertible.
Do not treat IsValid() as a general inverse-safety test. In the current upstream implementation, IsValid(epsilon) checks whether the two diagonal coefficients individually exceed epsilon; it does not test the full determinant or guarantee finite coefficients. A matrix with nonzero diagonal values can still be singular because of its shear terms. For inverse-dependent operations, validate finite coefficient values and evaluate determinant/conditioning with a tolerance suitable for the application’s coordinate scale before calling Invert() or ApplyInverse().
#include <AffineTransform.h>
#include <cmath>
bool
TryInverse(const BAffineTransform& input, BAffineTransform& output)
{
if (!std::isfinite(input.sx) || !std::isfinite(input.shy)
|| !std::isfinite(input.shx) || !std::isfinite(input.sy)
|| !std::isfinite(input.tx) || !std::isfinite(input.ty))
return false;
const double determinant = input.Determinant();
if (!std::isfinite(determinant) || std::fabs(determinant) < 1e-10)
return false; // Illustrative threshold; select it for your units.
BAffineTransform candidate = input;
candidate.Invert();
if (!std::isfinite(candidate.sx) || !std::isfinite(candidate.shy)
|| !std::isfinite(candidate.shx) || !std::isfinite(candidate.sy)
|| !std::isfinite(candidate.tx) || !std::isfinite(candidate.ty))
return false;
output = candidate;
return true;
}
This guard is illustrative, not a universal numeric policy. A robust graphics editor may need a relative condition estimate rather than a fixed determinant threshold. If coordinates are normalized near one, a tiny determinant is suspicious; if the application intentionally uses large or very small model units, choose a tolerance based on that domain. Also reject NaN or infinite coefficients before drawing, serializing, or transforming user input.
Keep the source transform unchanged when failure must be reported: invert a copy, validate, then publish the inverse. Test the composition round trip with representative points: inverse.Apply(forward.Apply(p)) should return within a documented tolerance of p. A passing test on the origin is weak evidence because translation-only mistakes can remain hidden there.
Drawing and input boundaries
Haiku drawing views already have view state and a drawing coordinate system. Choose whether a shape is transformed by the BView state or by a BAffineTransform on its data. Applying the same translation at both layers produces a double offset. Keep drawing and hit-testing paths symmetric: the forward mapping that positions an object and the inverse mapping that selects it should be derived from the same model transform.
If a transform is part of animation, calculate it from stable model values for each frame rather than accumulating floating-point mutations forever. Repeatedly applying a small rotation to the previous frame can drift, and repeatedly applying a scale compounds exponentially. Recompute from the initial transform and current animation parameter, or periodically re-normalize using a validated decomposition when the application’s math requires it.
GetAffineParameters() can extract translation, rotation, scale, and shear when decomposition is meaningful, but do not assume every matrix has a unique user-intuitive decomposition. Reflections, near-singular matrices, and mixed shear/scale can make representation ambiguous. If a settings UI promises separate X/Y scale and rotation fields, define how imported arbitrary matrices map into those controls and how round-trip errors are handled.
Flattening and reproducibility
BAffineTransform implements BFlattenable, including a type code and fixed-size flattened representation in the current API. For Haiku message transport or archiving, check Flatten() and Unflatten() statuses and validate the resulting coefficients before using them. A successful unflatten means bytes matched the flattenable contract; it does not prove that a matrix is invertible, sensible, or within an application’s allowed coordinate range.
For a long-lived document format, consider serializing named fields and a schema version rather than coupling user data to an internal flattening layout. Store units and transform direction explicitly. Add migrations when changing conventions. A tuple of six floating-point numbers without its source/destination spaces is difficult to interpret safely a year later.
Verification plan
Create a small test set with identity, translation, independent X/Y scales, quarter-turn rotation, reflection, shear, and a deliberately singular transform. For each, verify the six-coefficient mapping against hand-computed points. Then test transform composition by applying steps individually and comparing with the combined object, using non-origin, non-axis-aligned points. Test inverse round trips for well-conditioned cases and explicit rejection of singular/near-singular cases.
For UI integration, draw a marked rectangle or cross at known model coordinates, hit-test its center and outside points, resize the view, and confirm that model-to-view placement stays correct. Test transformed bounding boxes by rotating a non-square rectangle. Repeat at the minimum and maximum allowed model scales and with large translations to expose precision limits. When persisting transforms, reload them and run the same point-based regression suite.
The practical rule is to treat BAffineTransform as a precise mapping with a declared direction, not as a bag of six reusable numbers. Named spaces, tested composition, determinant-aware inverse handling, and round-trip tests keep graphics and input consistent.
Related:
- Haiku BShape: Constructing Reusable Vector Paths
- Haiku BRegion: Building Exact Clipping and Damage Geometry
Sources: