Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ced5a26
Add Sutherland-Hodgman algorithm documentation
peterstace Mar 27, 2026
6374c8c
Add exhaustive ClipByRect unit test case inventory
peterstace Mar 31, 2026
182d374
sutherland_hodgman.md: tweak test cases
peterstace Apr 1, 2026
1a07156
sutherland_hodgman.md: tweak test cases and rules
peterstace Apr 2, 2026
d2cecf9
sutherland_hodgman.md: tweaks
peterstace Apr 2, 2026
7bf04e6
alg_sutherland_hodgman.go: add stubs and dispatch for ClipByRect
peterstace Apr 2, 2026
1a32f86
Implement for Point
peterstace Apr 2, 2026
e7e56f3
MultiPoint
peterstace Apr 2, 2026
22cb4c5
LineString
peterstace Apr 3, 2026
e1c5957
MultiLineString
peterstace Apr 3, 2026
2f014f6
Implement polygon clipping
peterstace Apr 4, 2026
3e2f6fe
MultiPolygon
peterstace Apr 4, 2026
f9c03e6
polygonClipper refactor
peterstace Apr 4, 2026
18c63dd
GeometryCollection
peterstace Apr 7, 2026
35a9117
Add DR, NE, and CD test cases for ClipByRect
peterstace Apr 7, 2026
488fb3b
Fix Z/M values lost when polygon fully contains clipping rect
peterstace Apr 8, 2026
9353ebe
Replace []Coordinates with mutableSequence in polygon clipping
peterstace Apr 8, 2026
1fac7c7
Revert "Replace []Coordinates with mutableSequence in polygon clipping"
peterstace Apr 9, 2026
4e8582a
alg_clip_by_rect_sutherland_hodgman.go: Alt way to handle coordinates
peterstace Apr 9, 2026
71bc5cf
Revert back to using []Coordinates
peterstace Apr 10, 2026
f35ddea
Pass around raw []float64
peterstace Apr 10, 2026
8447c1e
Rename ClipByRect to ClipByRect2D with 2D-only output
peterstace May 22, 2026
7a6674b
Update ClipByRect tests to use consolidated test helpers
peterstace May 22, 2026
100ab7c
Fix lint failures from CI
peterstace May 23, 2026
cb7b00a
Use sort package instead of slices/cmp
peterstace May 23, 2026
fdfd0cb
Remove clip-by-rect algorithm docs
peterstace May 24, 2026
c4b3d3b
Rename Sutherland-Hodgman test file to match source
peterstace May 24, 2026
c5e97f0
Remove redundant endpoint short-circuits in lerpXY
peterstace May 24, 2026
4dbf7b5
Remove vertical whitespace
peterstace May 25, 2026
034e5a1
Trim lerpXY doc comment to its core contract
peterstace May 25, 2026
b3702d3
Move xysToSeq next to its inverse asXYs in type_sequence.go
peterstace May 25, 2026
cc7075d
Remove redundant empty-sequence guard in clipLineStringByRect
peterstace May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
- Add `NewEnvelopeXY` constructor for building an `Envelope` from variadic x/y
coordinate pairs, following the existing `New*XY` constructor pattern.

- Add `ClipByRect2D` function that clips a geometry to a 2D axis-aligned
rectangle (defined by an `Envelope`). It uses the Sutherland-Hodgman
algorithm for polygons and the Liang-Barsky algorithm for line strings. The
result is always 2D: any Z or M values on the input are discarded, avoiding
the ambiguity of synthesising Z/M values at rectangle corners introduced by
the clip.

## v0.59.0

2026-03-27
Expand Down
114 changes: 114 additions & 0 deletions geom/alg_clip_by_rect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package geom

// ClipByRect2D clips a geometry to the 2D axis-aligned rectangle defined by
// the given [Envelope], returning the portion of the geometry that lies within
// the rectangle. It uses the Sutherland-Hodgman algorithm for polygon clipping
// and the Liang-Barsky algorithm for line clipping.
//
// The result is always 2D ([DimXY]): any Z or M values on the input are
// discarded. Linear interpolation of Z/M at clip-edge intersections is
// reasonably well-defined, but values that would have to be synthesised at
// rectangle corners (where the clipped boundary must traverse the rect) are
// not, so this function avoids the problem by reducing to two dimensions
// throughout.
func ClipByRect2D(g Geometry, rect Envelope) Geometry {
return clipByRect2D(g.Force2D(), rect)
}

// clipByRect2D dispatches by geometry type. The input must already have been
// reduced to [DimXY] by the caller.
func clipByRect2D(g Geometry, rect Envelope) Geometry {
switch g.Type() {
case TypePoint:
return clipPointByRect(g.MustAsPoint(), rect).AsGeometry()
case TypeMultiPoint:
return clipMultiPointByRect(g.MustAsMultiPoint(), rect).AsGeometry()
case TypeLineString:
return clipLineStringByRect(g.MustAsLineString(), rect)
case TypeMultiLineString:
return clipMultiLineStringByRect(g.MustAsMultiLineString(), rect).AsGeometry()
case TypePolygon:
return clipPolygonByRect(g.MustAsPolygon(), rect)
case TypeMultiPolygon:
return clipMultiPolygonByRect(g.MustAsMultiPolygon(), rect).AsGeometry()
case TypeGeometryCollection:
return clipGeometryCollectionByRect(g.MustAsGeometryCollection(), rect).AsGeometry()
default:
panic("unknown geometry: " + g.Type().String())
}
}

func clipPointByRect(p Point, rect Envelope) Point {
xy, ok := p.XY()
if !ok {
return p
}
if rect.Contains(xy) {
return p
}
return Point{}
}

func clipMultiPointByRect(mp MultiPoint, rect Envelope) MultiPoint {
n := mp.NumPoints()
var pts []Point
for i := 0; i < n; i++ {
clipped := clipPointByRect(mp.PointN(i), rect)
if !clipped.IsEmpty() {
pts = append(pts, clipped)
}
}
return NewMultiPoint(pts)
}

func clipMultiLineStringByRect(mls MultiLineString, rect Envelope) MultiLineString {
n := mls.NumLineStrings()
var lines []LineString
for i := 0; i < n; i++ {
clipped := clipLineStringByRect(mls.LineStringN(i), rect)
switch clipped.Type() {
case TypeLineString:
ls := clipped.MustAsLineString()
if !ls.IsEmpty() {
lines = append(lines, ls)
}
case TypeMultiLineString:
lines = append(lines, clipped.MustAsMultiLineString().Dump()...)
default:
panic("unexpected type from clipLineStringByRect: " + clipped.Type().String())
}
}
return NewMultiLineString(lines)
}

func clipMultiPolygonByRect(mp MultiPolygon, rect Envelope) MultiPolygon {
n := mp.NumPolygons()
var polys []Polygon
for i := 0; i < n; i++ {
clipped := clipPolygonByRect(mp.PolygonN(i), rect)
switch clipped.Type() {
case TypePolygon:
p := clipped.MustAsPolygon()
if !p.IsEmpty() {
polys = append(polys, p)
}
case TypeMultiPolygon:
polys = append(polys, clipped.MustAsMultiPolygon().Dump()...)
default:
panic("unexpected type from clipPolygonByRect: " + clipped.Type().String())
}
}
return NewMultiPolygon(polys)
}

func clipGeometryCollectionByRect(gc GeometryCollection, rect Envelope) GeometryCollection {
n := gc.NumGeometries()
var geoms []Geometry
for i := 0; i < n; i++ {
clipped := clipByRect2D(gc.GeometryN(i), rect)
if !clipped.IsEmpty() {
geoms = append(geoms, clipped)
}
}
return NewGeometryCollection(geoms)
}
98 changes: 98 additions & 0 deletions geom/alg_clip_by_rect_liang_barsky.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package geom

func clipLineStringByRect(ls LineString, rect Envelope) Geometry {
emptyLine := LineString{}.AsGeometry()

seq := ls.Coordinates()
n := seq.Length()

lo, hi, ok := rect.MinMaxXYs()
if !ok {
return emptyLine
}

var chains [][]XY
var cur []XY

for i := 0; i < n-1; i++ {
a := seq.GetXY(i)
b := seq.GetXY(i + 1)

tMin, tMax, ok := clipSegmentParams(a, b, lo, hi)
if !ok {
if len(cur) > 0 {
chains = append(chains, cur)
cur = nil
}
continue
}

ca := lerpXY(a, b, tMin)
cb := lerpXY(a, b, tMax)

Comment on lines +30 to +32
if len(cur) > 0 && cur[len(cur)-1] == ca {
cur = append(cur, cb)
} else {
if len(cur) > 0 {
chains = append(chains, cur)
}
cur = []XY{ca, cb}
}
}
if len(cur) > 0 {
chains = append(chains, cur)
}

if len(chains) == 0 {
return emptyLine
}

lines := make([]LineString, len(chains))
for i, c := range chains {
lines[i] = NewLineString(xysToSeq(c))
}

if len(lines) == 1 {
return lines[0].AsGeometry()
}
return NewMultiLineString(lines).AsGeometry()
}

// clipSegmentParams uses the Liang-Barsky algorithm to compute the parametric
// range [tMin, tMax] of segment a->b that lies inside the axis-aligned
// rectangle from lo to hi. It returns false if no segment of positive length
// survives.
func clipSegmentParams(a, b, lo, hi XY) (float64, float64, bool) {
tMin := 0.0
tMax := 1.0
dx := b.X - a.X
dy := b.Y - a.Y
for _, pq := range [4][2]float64{
{-dx, a.X - lo.X}, // left
{dx, hi.X - a.X}, // right
{-dy, a.Y - lo.Y}, // bottom
{dy, hi.Y - a.Y}, // top
} {
p, q := pq[0], pq[1]
if p == 0 {
if q < 0 {
return 0, 0, false
}
continue
}
t := q / p
if p < 0 {
if t > tMin {
tMin = t
}
} else {
if t < tMax {
tMax = t
}
}
if tMin >= tMax {
return 0, 0, false
}
}
return tMin, tMax, true
}
Loading
Loading