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"Overview
Section titled “Overview”UXShapePath* tri = new UXShapePath();tri.moveTo(10, 10);tri.lineTo(90, 10);tri.lineTo(50, 80);tri.close();
tri.boundingBox(); // 10,10 80x70tri.containsPoint(50, 30); // truetri.containsPoint(5, 5); // falseIt 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(); // trueUXShapePath* flat = blob.flattened();flat.hasCurves(); // falseblob.hasCurves(); // still true — the original is untouchedFlattening 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 = 10blob.boundingBox(); // y = 20, not 10The 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:
| tolerance | a quarter pixel of chord deviation |
| depth cap | 9 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.
Sub-pixel output, and why
Section titled “Sub-pixel output, and why”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.
Containment is even-odd
Section titled “Containment is even-odd”containsPoint casts a horizontal ray and counts crossings.
This has two consequences:
ring.containsPoint(10, 50); // true — in the wallring.containsPoint(50, 50); // false — in the holeA 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() anywhereclose() still matters for stroking: an unclosed path has two loose ends,
and caps go on ends.
Caps belong to the path
Section titled “Caps belong to the path”arrow.setEndCap(UXCAP_ARROW);arrow.setCapWidth(6);UXCAP_NONE | stop dead at the endpoint (butt) |
UXCAP_ROUND | a half-disc of the stroke width |
UXCAP_SQUARE | a half-width square extension |
UXCAP_ARROW | an 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.
outline.setJoin(UXJOIN_MITER);UXJOIN_MITER | the two edges extended to a point (a sharp corner) |
UXJOIN_ROUND | the default: a disc fills the outside of every turn |
UXJOIN_BEVEL | the corner cut flat |
A join is where the two segments of a turn meet, and like a cap it belongs to
the shape, not the backend — a nation’s border is drawn mitred or it is a
different border. The default is round: the toolkit’s own stroker covers the
outside of a turn with a disc, and round is what that produces, so a path that
never sets a join draws the way it always did. The native strokers pass the
join straight through (NSLineJoinStyle, PS_JOIN_*, kCGLineJoin*).
Dashes
Section titled “Dashes”i32 pat[2];pat[0] = (i32)8;pat[1] = (i32)8;outline.setDash(&pat[0], (i32)2, (i32)0); // 8 on, 8 off, starting at the run's startAn on/off run in whole device pixels plus a phase — how far into the run the stroke starts. It lives on the path for the same reason the caps and the join do: it is a property of the shape the author drew. The dash reaches the seam as an argument rather than as context state, because it changes every frame (a coastline crawler animates it), and state would leak it into the next stroke that did not set one.
The phase restarts at every subpath: a move starts a fresh run. That is what Canvas2D and SVG do, and what every native dasher measured does.
n <= 0 clears the run back to solid, a non-positive entry is lifted to
1, and entries past UX_DASH_MAX (8) are dropped rather than overflowing
the buffer. flattened() carries the run, so a copy of the
path strokes the same.
Whether a backend lays the dash down itself is
UXGraphics.dashesNatively.
Where it answers false, UXPainter dashes
the flattened centreline instead.
Topics
Section titled “Topics”moveTo · lineTo · curveTo · quadTo · close · rect · roundRect · hasCurves · flattened · boundingBox · edges · containsPoint · setStartCap · setEndCap · setCapWidth · setArrowLength · capOutline · setJoin · joinKind · setDash · clearDash · dashCount · dashAt · dashPhase
moveTo
Section titled “moveTo”void moveTo(i16 x, i16 y)Start a new subpath. A path may have any number.
lineTo
Section titled “lineTo”void lineTo(i16 x, i16 y)curveTo
Section titled “curveTo”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.
quadTo
Section titled “quadTo”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.
roundRect
Section titled “roundRect”static UXShapePath* roundRect(i16 x, i16 y, i16 w, i16 h, i32 r)A rectangle with corners of radius r, each a quarter circle drawn as a cubic.
The radius is clamped to half the shorter side, and 0 gives rect.
elementCount / elemAt
Section titled “elementCount / elemAt”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.
startCapKind / endCapKind / arrowLength
Section titled “startCapKind / endCapKind / arrowLength”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.
hasCurves
Section titled “hasCurves”bool hasCurves(void)Whether any element is a curve. When false, flattened is a
no-op.
flattened
Section titled “flattened”UXShapePath* flattened(void)A new path of straight lines only. The receiver is unchanged.
boundingBox
Section titled “boundingBox”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.
containsPoint
Section titled “containsPoint”bool containsPoint(i16 px, i16 py)Even-odd containment. See above.
setStartCap / setEndCap
Section titled “setStartCap / setEndCap”void setStartCap(i32 c)void setEndCap(i32 c)setCapWidth
Section titled “setCapWidth”void setCapWidth(i16 w)The stroke width the caps are built for. 0 means ask at cap time.
setArrowLength
Section titled “setArrowLength”void setArrowLength(i16 n)Arrowhead length along the direction of travel. 0 means three times the width.
capOutline
Section titled “capOutline”UXShapePath* capOutline(bool atStart, double width)The cap as its own path, ready to fill. Null when that end has no cap (a closed
subpath, or UXCAP_NONE).
setJoin
Section titled “setJoin”void setJoin(i32 j)The join at each turn: UXJOIN_MITER, UXJOIN_ROUND or UXJOIN_BEVEL. See
Joins.
joinKind
Section titled “joinKind”i32 joinKind(void)Read back the join, the way startCapKind reads back a cap.
setDash
Section titled “setDash”void setDash(i32* pat, i32 n, i32 phase)The dash run, its entry count and the phase into it. n <= 0 clears it back
to solid. See Dashes.
clearDash
Section titled “clearDash”void clearDash(void)Back to a solid stroke.
dashCount
Section titled “dashCount”i32 dashCount(void)The number of run entries — 0 when the stroke is solid.
dashAt
Section titled “dashAt”i32 dashAt(i32 i)One entry of the run, in device pixels.
dashPhase
Section titled “dashPhase”i32 dashPhase(void)The phase into the run. It may be negative; whoever consumes it reduces it into the run.
Example
Section titled “Example”triangle: x=10 y=10 w=80 h=70 edges=3inside (50,30)=1 outside (5,5)=0 outside (50,90)=0unclosed: x=10 y=10 w=80 h=70 edges=3still contains (50,30): 1blob has curves: 1blob: x=20 y=20 w=60 h=60 edges=70flattened has curves: 0 edges=70original still curved: 1ring: x=0 y=0 w=100 h=100 edges=8in the wall (10,50)=1 in the hole (50,50)=0empty: x=0 y=0 w=0 h=0 edges=0The 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.
Conforms to
Section titled “Conforms to”- A plain class (not an
Objectsubclass)
See also
Section titled “See also”UXPathElement: one command in the pathUXEdge: one line segmentUXRect: whatboundingBoxreturnsUXGraphics: the drawing seam these are stroked through