Skip to content

UXShapePath

UXShapePath is a vector path: moveTo, lineTo, curveTo, quadTo, close. Ask it for a bounding box, for its edges, or whether a point is inside.

It is pure geometry with no driver or backend to boot, so a custom-shaped control can hit-test itself in a unit test.

#use <UXKit> // or #import "UXShapePath.xc"
UXShapePath* tri = new UXShapePath();
tri.moveTo(10, 10);
tri.lineTo(90, 10);
tri.lineTo(50, 80);
tri.close();
tri.boundingBox(); // 10,10 80x70
tri.containsPoint(50, 30); // true
tri.containsPoint(5, 5); // false

It is shaped like NSBezierPath. Coordinates are i16, which is the authoring range; see sub-pixel units for where the precision lives.

Curves are stored, not flattened on the way in

Section titled “Curves are stored, not flattened on the way in”

A path keeps the cubic you drew. flattened produces the polyline on demand:

blob.hasCurves(); // true
UXShapePath* flat = blob.flattened();
flat.hasCurves(); // false
blob.hasCurves(); // still true — the original is untouched

Flattening is lossy and resolution-dependent. The same path may want 8 segments in a 40-pixel thumbnail and 200 in a printout. A path that discarded its curves at build time could only answer at one of them.

Every consumer here (boundingBox, edges, containsPoint) routes through flattened(), so a curved path behaves the same as the straight-line path it approximates. There is no separate curve code path to disagree with the line one.

The bounding box is of the curve, not the control points

Section titled “The bounding box is of the curve, not the control points”
blob.moveTo(20, 50);
blob.curveTo(20, 10, 80, 10, 80, 50); // control points at y = 10
blob.boundingBox(); // y = 20, not 10

The box is measured from the flattened polyline, so it is the box the shape occupies. A control-point box would be ten pixels too large here, and a hit test built on it would accept clicks in empty space.

Integer flattening, and why it is de Casteljau

Section titled “Integer flattening, and why it is de Casteljau”

The flattener subdivides: it halves the curve, tests whether the halves are flat enough to be lines, and recurses if not. All arithmetic is integer.

Halving is add-and-shift, so it cannot overflow. Evaluating B(t) at t = i/n needs n³ × coordinate, which leaves i32 at around n = 16, and xt has no 64-bit integer. Integer arithmetic also means every backend flattens a given path to the same pixels, instead of each rounding a float its own way.

Two constants bound it:

tolerancea quarter pixel of chord deviation
depth cap9 levels — at most 512 segments per curve

The deviation is measured against the true chord length, not a Manhattan length. The Manhattan length over-estimates the true length by up to 41%, which would loosen a whole-pixel tolerance to about 1.4 px and turn a 60-pixel blob into a visible octagon. The depth cap stops a pathological curve subdividing for ever.

A path stores whole-pixel coordinates, which suits authoring. A stroke built from whole-pixel vertices has a silhouette that wobbles by up to half a pixel, and a one-pixel wobble is visible on every backend, antialiased or not. Hard pixels on GEM do not hide it.

The flattener can therefore also emit 1/16 px units. The neutral stroker works in these and rounds once, at the end.

containsPoint casts a horizontal ray and counts crossings. This has two consequences:

ring.containsPoint(10, 50); // true — in the wall
ring.containsPoint(50, 50); // false — in the hole

A second subpath inside the first is a hole. This is even-odd fill, and it cuts a hole without a boolean operation.

Every subpath is also implicitly closed for containment, so a path you did not close() still hit-tests as the shape you drew:

open.containsPoint(50, 30); // true, with no close() anywhere

close() still matters for stroking: an unclosed path has two loose ends, and caps go on ends.

arrow.setEndCap(UXCAP_ARROW);
arrow.setCapWidth(6);
UXCAP_NONEstop dead at the endpoint (butt)
UXCAP_ROUNDa half-disc of the stroke width
UXCAP_SQUAREa half-width square extension
UXCAP_ARROWan arrowhead pointing the way the path was going

Caps live here and not on a backend because a cap is a property of the shape the author drew, whatever renders it. The cap geometry uses the same integer arithmetic as the rest of this page. A closed subpath has no ends and therefore no caps.

moveTo · lineTo · curveTo · quadTo · close · rect · hasCurves · flattened · boundingBox · edges · containsPoint · setStartCap · setEndCap · setCapWidth · setArrowLength · capOutline

void moveTo(i16 x, i16 y)

Start a new subpath. A path may have any number.

void lineTo(i16 x, i16 y)
void curveTo(i16 c1x, i16 c1y, i16 c2x, i16 c2y, i16 x, i16 y)

A cubic Bézier: two off-curve control points, then the on-curve end.

void quadTo(i16 cx, i16 cy, i16 x, i16 y)

A quadratic, elevated to a cubic on the way in, so there is one curve type to flatten. hasCurves() becomes true.

void close(void)

Close the current subpath. Affects stroking and caps; containment closes implicitly either way.

static UXShapePath* rect(i16 x, i16 y, i16 w, i16 h)

A shortcut for the commonest shape.

i32 elementCount(void)
UXPathElement* elemAt(i32 i)

Walk the path’s commands. A path editor uses these to draw the handles, serialise the shape, or re-emit it transformed. They read the path and do not build it; see UXPathElement for what each element carries.

void add(i32 type, i16 x, i16 y)

Append a raw element. moveTo, lineTo and close are written in terms of it. Use it to replay a path you walked with elemAt.

It takes no control points, so it cannot append a curve. UXPE_CURVE needs curveTo.

i32 startCapKind(void)
i32 endCapKind(void)
i16 arrowLength(void)

Read back what setStartCap and the related setters set. The stroker uses them; an application reads them to show the current cap in an inspector.

bool hasCurves(void)

Whether any element is a curve. When false, flattened is a no-op.

UXShapePath* flattened(void)

A new path of straight lines only. The receiver is unchanged.

UXRect boundingBox(void)

The UXRect the flattened shape occupies. An empty path gives an empty box.

Array<UXEdge>* edges(void)

The path as line segments, with each subpath closed. Zero-length segments are dropped, so consecutive identical points do not become degenerate edges.

bool containsPoint(i16 px, i16 py)

Even-odd containment. See above.

void setStartCap(i32 c)
void setEndCap(i32 c)
void setCapWidth(i16 w)

The stroke width the caps are built for. 0 means ask at cap time.

void setArrowLength(i16 n)

Arrowhead length along the direction of travel. 0 means three times the width.

UXShapePath* capOutline(bool atStart, i16 width)

The cap as its own path, ready to fill. Null when that end has no cap (a closed subpath, or UXCAP_NONE).

triangle: x=10 y=10 w=80 h=70 edges=3
inside (50,30)=1 outside (5,5)=0 outside (50,90)=0
unclosed: x=10 y=10 w=80 h=70 edges=3
still contains (50,30): 1
blob has curves: 1
blob: x=20 y=20 w=60 h=60 edges=70
flattened has curves: 0 edges=70
original still curved: 1
ring: x=0 y=0 w=100 h=100 edges=8
in the wall (10,50)=1 in the hole (50,50)=0
empty: x=0 y=0 w=0 h=0 edges=0

The blob’s control points sit at y = 10 and its box starts at y = 20, the curve’s real extent. The program is website/site/examples/uxkit/shapes.xc; the doc-examples gate compiles it, and the output above is its real output.

  • A plain class (not an Object subclass)