This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Distance Queries
Besides whole fields, cl-sdf answers questions about single points: is it inside, and how far is it from the edge. These suit hit testing, collision, placing things along an outline, and checking fields.
The examples use *square* from Home, *same-way* from Contours and Fill Rules, and rectangle-contour from Recipes.
Prepared shapes
(sdf:make-shape contours &key (fill-rule :nonzero) (flatten-tolerance 0.01)) ; => shape
make-shape flattens the contours, resolves the filled region's boundary, and indexes both in grids, once. Queries on the shape then look only at segments near the point. The contours must be closed; an open one signals open-contour-error.
:flatten-tolerance is in shape units, 0.01 by default. Point queries have no texel size to scale it by, so choose it for your units: 0.01 suits outlines about one unit across, such as glyphs in ems, and is far too fine for an outline in font units of 2048 per em, or too coarse for one in kilometres.
(defparameter *shape* (sdf:make-shape *square*))
(sdf:shape-fill-rule *shape*) ; => :NONZERO
(sdf:shape-bounds *shape*) ; => (0.0d0 0.0d0 1.0d0 1.0d0), control-point box
(sdf:shape-p *shape*) ; => T
A shape's fill rule and tolerance are fixed when it is made. Shapes are immutable, and may be shared and queried from any number of threads at once.
The queries
All three take a shape or contours, and a point as two reals within ±1e36, the type sdf:coordinate; a point further out is a type-error.
(sdf:inside-p *shape* 0.5 0.5) ; => T
(sdf:signed-distance *shape* 0.5 0.25) ; => 0.25d0
(sdf:signed-distance *shape* 2 2) ; => -1.4142135623730951d0
(sdf:unsigned-distance *shape* 2 2) ; => 1.4142135623730951d0
| Function | Returns | Measures to |
|---|---|---|
(inside-p shape x y) |
true or false | The fill rule, over every input segment. |
(signed-distance shape x y) |
double-float, positive inside | The filled region's boundary only. |
(unsigned-distance shape x y) |
double-float, never negative | Every input segment, internal ones included. |
The difference between the last two matters for overlapping contours. signed-distance ignores edges inside the filled region; unsigned-distance does not:
(defparameter *two* (list (rectangle-contour 0 0 2 1) (rectangle-contour 1 0 3 1)))
(sdf:signed-distance *two* 1.25 0.5) ; => 0.5d0, to the top and bottom
(sdf:unsigned-distance *two* 1.25 0.5) ; => 0.25d0, to the hidden edge at x = 1
A point exactly on the boundary may be classified either way, with a distance of zero.
Queries on contours
Every query also accepts contours in place of a shape, with :fill-rule (not for unsigned-distance) and :flatten-tolerance applying to that one call:
(sdf:inside-p *same-way* 0 0 :fill-rule :even-odd) ; => NIL
This prepares the contours on every call. For inside-p and unsigned-distance that is a flattening, a check that the contours close (for inside-p) and an index, cheap for a single question. signed-distance makes a whole shape each time, which costs as much as make-shape: for a glyph, about a thousand times a query on a prepared shape. Make a shape for anything more than a few queries.
Passing :fill-rule or :flatten-tolerance together with a shape signals an sdf-error, since the shape's own would silently win.
Open paths
Only unsigned-distance takes open contours: it measures to open paths and to isolated points (segments of zero length) as well. inside-p, make-shape and signed-distance need closed contours, since an open one has no inside, and signal open-contour-error for one.
Empty shapes
With no segments at all, there is no edge to measure to:
| Query | Result |
|---|---|
inside-p |
nil |
unsigned-distance |
3.4028234663852886d38, most-positive-single-float as a double-float |
signed-distance |
-3.4028234663852886d38 |
The value is finite so that it survives arithmetic and storage in single-floats.
Encoding distances
encode-distance and decode-distance are the mapping fields use, for building or reading samples yourself:
(sdf:encode-distance distance spread &key (clamp t)) ; => single-float
(sdf:decode-distance value spread) ; => double-float
(sdf:encode-distance 0.05 0.1) ; => 0.75
(sdf:encode-distance 0.5 0.1) ; => 1.0, clamped
(sdf:encode-distance 0.5 0.1 :clamp nil) ; => 3.0
(sdf:decode-distance 0.75 0.1) ; => 0.05000000074505806d0
encode-distance is 0.5 + distance / (2 × spread), clamped to 0..1 unless :clamp nil. decode-distance is its inverse, without clamping. The spread must be positive. The decoded 0.05 above is not exact because 0.75 and 0.1 are single-floats.
These two are arithmetic, not geometry, so they take any reals within the double-float range, such as a distance of 1d300. A result that does not fit, an unclamped sample beyond the single-float range or a decoded distance beyond the double-float range, signals an sdf-error rather than a floating-point overflow.
Cost
On a prepared glyph, a query takes well under a microsecond; inside-p is the cheapest. It allocates nothing, and signed-distance and unsigned-distance allocate only the double-float they return, 16 bytes, so queries can run every frame. make-shape takes from a few hundredths of a millisecond to a few tenths for a Latin glyph. See Accuracy and Performance.