Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BShape: Constructing Reusable Vector Paths

Model Haiku vector geometry with BShape contours, checked path operations, measured bounds, transforms, and reproducible drawing tests.

BShape is an Interface Kit data object for ordered path geometry. It records path operations such as moving to a point, drawing straight or Bezier segments, adding an arc, and closing a contour. A shape is not itself a view, a bitmap, or a drawing command sent to the Application Server. It becomes visible when a view uses it with operations such as StrokeShape() or FillShape().

That separation is valuable: application code can construct, copy, archive, transform, and test a path independently from the view that renders it. The tradeoff is that callers must own the coordinate convention, path lifecycle, fill/stroke choice, and error handling. A successfully constructed sequence still does not guarantee a visually correct result.

Build contours explicitly

Start a contour with MoveTo(), then append line, curve, or arc segments. Each mutating operation returns status_t; propagate or handle errors rather than assuming allocation always succeeds. Close() closes the current contour back to its start for operations that need a closed boundary. Multiple contours can represent separate islands or cutouts, but fill interpretation depends on the drawing API and path winding. Do not treat a shape as a semantic region or infer boolean-union behavior from AddShape().

#include <Point.h>
#include <Shape.h>
#include <SupportDefs.h>

status_t
BuildBadge(BShape& shape)
{
    status_t status = shape.MoveTo(BPoint(8, 24));
    if (status != B_OK)
        return status;

    status = shape.LineTo(BPoint(24, 8));
    if (status != B_OK)
        return status;

    status = shape.BezierTo(BPoint(34, 8), BPoint(40, 14), BPoint(40, 24));
    if (status != B_OK)
        return status;

    status = shape.LineTo(BPoint(24, 40));
    if (status != B_OK)
        return status;

    return shape.Close();
}

The points above are in the shape’s local coordinate system. The function does not create a view, choose a pen, or guarantee that coordinates fit the eventual view bounds. It also leaves policy to its caller: on a failure after partial construction, the caller can discard the local shape or call Clear() before reuse. Prefer constructing into a temporary and publishing it only after every operation succeeds; that prevents other code from seeing half a path.

BezierTo() takes three points: two control points and an endpoint. The current point is the start of the curve. Control points influence the curve but are not generally points the rendered line passes through. A common bug is to mistake them for three successive vertices or to omit MoveTo() before the first segment. Build helper functions that make the path grammar explicit and keep each contour’s start and end visible in code review.

Reuse, append, and ownership

BShape supports copy construction, assignment, moving, Clear(), and AddShape(). Treat it as a value object whose copy owns its own path data. AddShape() appends another shape’s operation and point sequences; it does not perform a geometric union, weld touching endpoints, simplify curves, or normalize contour direction. Check its returned status and ensure the source shape remains alive for the call.

Use a local builder to keep invalid intermediate state private. A drawing model can cache the finished path when geometry changes and reuse it across repaint calls. Rebuilding the same points on every Draw() wastes allocations and makes redraw correctness harder to reason about. Conversely, do not mutate a shared shape while another thread or view is reading it unless the application establishes the required synchronization. BShape’s value semantics do not imply concurrent mutation safety.

When a path depends on model values, compute those values before entering a short locked drawing section. Keep UI ownership clear: a window looper can own the cached path and replace it when a message arrives; a worker can prepare an independent temporary shape and send a completed value/result back. Avoid sharing a mutable shape across threads merely to skip one copy. If profiling shows copying matters, measure it and design an immutable publication boundary.

Bounds are a layout aid, not a rendering oracle

Bounds() returns a BRect enclosing the points stored in the shape. The API reference explicitly warns that this implementation does not take curves into account. Do not treat it as an exact curve-extrema calculation or pixel-exact ink bounds. For a cubic Bezier, control points can lie away from the visible curve; a bounding box of stored points is only a rough geometry aid. For damage/invalidation, compute a safe curve bound or use a deliberately conservative extent, then account for stroke width, joins, antialiasing, and transforms.

For precise hit testing or clipping, decide what “inside” means for the product. A bounding rectangle is only a cheap broad-phase test, and this API’s Bounds() is not guaranteed to enclose the actual curve geometry. It also cannot distinguish empty gaps between disjoint contours or concave cutouts. For exact point-in-path behavior, use a documented path containment API if the target SDK provides one, or implement and test the desired winding/even-odd rule. Do not substitute Bounds().Contains(point) for exact geometric containment.

The same distinction applies to invalidation. Expand conservative damage bounds by the maximum pen width, cap/join effect, and any antialiasing margin used by the renderer. If a transform is applied, transform the corners of the local bound and recompute the enclosing axis-aligned rectangle; simply translating the original rectangle is wrong for rotation or shear. Keep a reference image test for curved shapes at multiple scales to catch clipping at extrema.

Coordinate systems and drawing state

Keep geometry in a stable local coordinate system, then place it using the view’s transform or by transforming a copy of the path when the drawing API requires that. This makes a reusable icon independent of one window’s origin. Document whether coordinates are logical view units, device pixels, or model units. Haiku view coordinates and screen scaling should be tested through the actual drawing path rather than guessed from a screenshot.

Drawing methods are affected by view state: high/low colors, pattern, pen size, clipping, drawing mode, and transform all matter. A reusable shape should not silently depend on state left behind by a previous helper. Set the required drawing state at the rendering boundary and restore or deliberately manage state so unrelated drawing code is not contaminated. Use a stroke-only preview while debugging path order; fill can obscure the distinction between correct contours and accidental closures.

Do not assume every closed path produces a hole or that overlaying one BShape on another subtracts geometry. When a compound icon includes a hole, test the exact fill behavior of the chosen BView API and contour direction. If the desired behavior is not directly supported by the documented API, use explicit clipping/masks or another well-defined representation rather than relying on an undocumented renderer detail.

Archive and compatibility boundaries

BShape is archivable through a BMessage, but an archive is an implementation-level object representation, not automatically the best long-term application file format. For user documents or interchange, define an explicit schema with a version, coordinate units, operation sequence, and validation limits. Treat untrusted archive data as data that may fail to unflatten; validate counts and coordinate ranges before accepting it into a live model.

When changing an application’s shape schema, preserve backward compatibility intentionally. Store named application fields and reconstruct the BShape through checked operations, or maintain a migration from prior archive versions. Keep a golden test shape with lines, curves, closure, and multiple contours so upgrade code is checked against known geometry. Do not assume a future Haiku release will preserve undocumented internal byte layouts merely because an archived object works in one build.

A verification workflow

Build a small view that renders the shape both with a thin stroke and a fill. Test every operation’s returned status, render after resize and expose events, and verify that the cached shape is regenerated only after model geometry changes. Draw the same shape at the origin and at a translated/rotated location to expose coordinate assumptions. Confirm that clipping bounds include the stroke and transformed corners.

Add unit-style tests for path construction: expected current position after each segment, expected operation count through a shape iterator when available, bounds for straight-line cases, copy independence, Clear(), and failure handling. For curves, compare rendered output or sample points against a trusted mathematical reference; Bounds() alone cannot validate curve quality. Test multiple contours and explicit closure when filling. Check behavior on the exact Haiku SDK you ship because API availability and drawing details can vary across releases.

The key contract is narrow and useful: BShape stores vector path instructions and provides value-like operations. Application code remains responsible for geometric meaning, safe publication, drawing state, hit testing, persistence schema, and the tests that prove those choices.

Related:

Sources:

Comments