Skip to content

Commit 8b4e8c1

Browse files
committed
Adding ShapeFilter options.
Space.Shapes() now returns a ShpaeCollection. Space.Width() / Height() is now in pixels, and I'm adding Space.WidthInCells / HeightInCells(). Adding Bounds.MinAxis() / MaxAxis(). FIX: Bounds.Intersection() was not returning the right intersecting bounds between two Bounds objects. FIX: LineTest now no longer panics if the OnIntersect callback is not set. FIX: Vector.Angle() is now a signed angle, rather than unsigned. Adding ShapeFilter.First() / Last(). Renaming NewRectangleTopLeft > NewRectangleFromTopLeft. Minor documentation fixes.
1 parent b5e1d3a commit 8b4e8c1

9 files changed

+180
-59
lines changed

convexPolygon.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,11 @@ func NewRectangle(x, y, w, h float64) *ConvexPolygon {
631631
)
632632
}
633633

634-
// NewRectangleTopLeft returns a rectangular ConvexPolygon at the position given with the vertices ordered in clockwise order.
634+
// NewRectangleFromTopLeft returns a rectangular ConvexPolygon at the position given with the vertices ordered in clockwise order.
635635
// The Rectangle's origin will be the center of its shape (as is recommended for collision testing).
636636
// Note that the rectangle will be positioned such that x, y is the top-left corner, though the center-point is still
637637
// in the center of the ConvexPolygon shape.
638-
func NewRectangleTopLeft(x, y, w, h float64) *ConvexPolygon {
638+
func NewRectangleFromTopLeft(x, y, w, h float64) *ConvexPolygon {
639639

640640
r := NewRectangle(x, y, w, h)
641641
r.Move(w/2, h/2)

examples/worldBouncer.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ func (w *WorldBouncer) Init() {
4545

4646
// Create a selection of shapes that comprise the walls.
4747
w.Solids = resolv.ShapeCollection{
48-
resolv.NewRectangleTopLeft(0, 0, 640, 16),
49-
resolv.NewRectangleTopLeft(0, 360-16, 640, 16),
50-
resolv.NewRectangleTopLeft(0, 16, 16, 360-16),
51-
resolv.NewRectangleTopLeft(640-16, 16, 16, 360-16),
52-
resolv.NewRectangleTopLeft(64, 128, 16, 200),
53-
resolv.NewRectangleTopLeft(120, 300, 200, 8),
48+
resolv.NewRectangleFromTopLeft(0, 0, 640, 16),
49+
resolv.NewRectangleFromTopLeft(0, 360-16, 640, 16),
50+
resolv.NewRectangleFromTopLeft(0, 16, 16, 360-16),
51+
resolv.NewRectangleFromTopLeft(640-16, 16, 16, 360-16),
52+
resolv.NewRectangleFromTopLeft(64, 128, 16, 200),
53+
resolv.NewRectangleFromTopLeft(120, 300, 200, 8),
5454
}
5555

5656
// Set their tags (not strictly necessary here because the bouncers bounce off of everything and anything)..

examples/worldCircleTest.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ func (w *WorldCircle) Init() {
2727
w.space = resolv.NewSpace(640, 360, 16, 16)
2828

2929
solids := resolv.ShapeCollection{
30-
resolv.NewRectangleTopLeft(0, 0, 640, 16),
31-
resolv.NewRectangleTopLeft(0, 360-16, 640, 16),
32-
resolv.NewRectangleTopLeft(0, 16, 16, 360-16),
33-
resolv.NewRectangleTopLeft(640-16, 16, 16, 360-16),
34-
resolv.NewRectangleTopLeft(64, 128, 16, 200),
35-
resolv.NewRectangleTopLeft(120, 300, 200, 8),
30+
resolv.NewRectangleFromTopLeft(0, 0, 640, 16),
31+
resolv.NewRectangleFromTopLeft(0, 360-16, 640, 16),
32+
resolv.NewRectangleFromTopLeft(0, 16, 16, 360-16),
33+
resolv.NewRectangleFromTopLeft(640-16, 16, 16, 360-16),
34+
resolv.NewRectangleFromTopLeft(64, 128, 16, 200),
35+
resolv.NewRectangleFromTopLeft(120, 300, 200, 8),
3636

3737
resolv.NewLine(256-32, 180-32, 256+32, 180+32),
3838
resolv.NewLine(256-32, 180+32, 256+32, 180-32),

examples/worldPlatformer.go

+14-14
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,18 @@ func (w *WorldPlatformer) Init() {
3636
solids := resolv.ShapeCollection{
3737
resolv.NewCircle(128, 128, 32),
3838

39-
resolv.NewRectangleTopLeft(0, 0, 640, 8),
40-
resolv.NewRectangleTopLeft(640-8, 8, 8, 360-16),
39+
resolv.NewRectangleFromTopLeft(0, 0, 640, 8),
40+
resolv.NewRectangleFromTopLeft(640-8, 8, 8, 360-16),
4141

42-
resolv.NewRectangleTopLeft(0, 8, 8, 360-32),
43-
resolv.NewRectangleTopLeft(0, 360-8-16, 8, 8),
44-
resolv.NewRectangleTopLeft(0, 360-8-8, 8, 8),
42+
resolv.NewRectangleFromTopLeft(0, 8, 8, 360-32),
43+
resolv.NewRectangleFromTopLeft(0, 360-8-16, 8, 8),
44+
resolv.NewRectangleFromTopLeft(0, 360-8-8, 8, 8),
4545

46-
resolv.NewRectangleTopLeft(64, 200, 300, 8),
47-
resolv.NewRectangleTopLeft(64, 280, 300, 8),
48-
resolv.NewRectangleTopLeft(512, 96, 32, 200),
46+
resolv.NewRectangleFromTopLeft(64, 200, 300, 8),
47+
resolv.NewRectangleFromTopLeft(64, 280, 300, 8),
48+
resolv.NewRectangleFromTopLeft(512, 96, 32, 200),
4949

50-
resolv.NewRectangleTopLeft(0, 360-8, 640, 8),
50+
resolv.NewRectangleFromTopLeft(0, 360-8, 640, 8),
5151
}
5252

5353
solids.SetTags(TagSolidWall | TagPlatform)
@@ -57,10 +57,10 @@ func (w *WorldPlatformer) Init() {
5757
/////
5858

5959
platforms := resolv.ShapeCollection{
60-
resolv.NewRectangleTopLeft(400, 200, 32, 16),
61-
resolv.NewRectangleTopLeft(400, 240, 32, 16),
62-
resolv.NewRectangleTopLeft(400, 280, 32, 16),
63-
resolv.NewRectangleTopLeft(400, 320, 32, 16),
60+
resolv.NewRectangleFromTopLeft(400, 200, 32, 16),
61+
resolv.NewRectangleFromTopLeft(400, 240, 32, 16),
62+
resolv.NewRectangleFromTopLeft(400, 280, 32, 16),
63+
resolv.NewRectangleFromTopLeft(400, 320, 32, 16),
6464
}
6565

6666
platforms.SetTags(TagPlatform)
@@ -87,7 +87,7 @@ func (w *WorldPlatformer) Init() {
8787
r.SetPositionVec(resolv.NewVector(240, 344))
8888
w.space.Add(r)
8989

90-
w.MovingPlatform = resolv.NewRectangleTopLeft(550, 200, 32, 8)
90+
w.MovingPlatform = resolv.NewRectangleFromTopLeft(550, 200, 32, 8)
9191
w.MovingPlatform.Tags().Set(TagPlatform)
9292
w.space.Add(w.MovingPlatform)
9393

shape.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,12 @@ func (s *ShapeBase) ID() uint32 {
7979
return s.id
8080
}
8181

82-
// Data returns any auxiliary data set on the Circle shape.
82+
// Data returns any auxiliary data set on the shape.
8383
func (s *ShapeBase) Data() any {
8484
return s.data
8585
}
8686

87-
// SetData sets any auxiliary data on the Circle shape.
87+
// SetData sets any auxiliary data on the shape.
8888
func (s *ShapeBase) SetData(data any) {
8989
s.data = data
9090
}
@@ -94,7 +94,7 @@ func (s *ShapeBase) Tags() *Tags {
9494
return s.tags
9595
}
9696

97-
// Move translates the Circle by the designated X and Y values.
97+
// Move translates the Shape by the designated X and Y values.
9898
func (s *ShapeBase) Move(x, y float64) {
9999
s.position.X += x
100100
s.position.Y += y

shapefilter.go

+93-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package resolv
22

3+
import (
4+
"reflect"
5+
"sort"
6+
)
7+
38
// ShapeIterator is an interface that defines a method to iterate through Shapes.
49
// Any object that has such a function (e.g. a ShapeFilter or a ShapeCollection (which is essentially just a slice of Shapes)) fulfills the ShapeIterator interface.
510
type ShapeIterator interface {
@@ -22,16 +27,13 @@ func (s ShapeFilter) ForEach(forEachFunc func(shape IShape) bool) {
2227

2328
s.operatingOn.ForEach(func(shape IShape) bool {
2429

25-
pass := true
26-
2730
for _, f := range s.Filters {
2831
if !f(shape) {
29-
pass = false
3032
return true
3133
}
3234
}
3335

34-
if pass && !forEachFunc(shape) {
36+
if !forEachFunc(shape) {
3537
return true
3638
}
3739

@@ -49,6 +51,15 @@ func (s ShapeFilter) ByTags(tags Tags) ShapeFilter {
4951
return s
5052
}
5153

54+
// NotByTags adds a filter to the ShapeFilter that filters out Shapes by tags (so only Shapes that DO NOT have the specified Tag(s) pass the filter).
55+
// The function returns the ShapeFiler for easy method chaining.
56+
func (s ShapeFilter) NotByTags(tags Tags) ShapeFilter {
57+
s.Filters = append(s.Filters, func(s IShape) bool {
58+
return !s.Tags().Has(tags)
59+
})
60+
return s
61+
}
62+
5263
// ByDistance adds a filter to the ShapeFilter that filters out Shapes distance to a given point.
5364
// The shapes have to be at least min and at most max distance from the given point Vector.
5465
// The function returns the ShapeFiler for easy method chaining.
@@ -67,6 +78,22 @@ func (s ShapeFilter) ByFunc(filterFunc func(s IShape) bool) ShapeFilter {
6778
return s
6879
}
6980

81+
// ByDataType allows you to filter Shapes by their Data pointer's type. You could use this to, for example, filter out Shapes that have
82+
// Data objects that are Updatable, where `Updatable` is an interface that has an `Update()` function call.
83+
// To do this, you would call `s.ByDataType(reflect.TypeFor[Updatable]())`
84+
func (s ShapeFilter) ByDataType(dataType reflect.Type) ShapeFilter {
85+
if dataType == nil {
86+
return s
87+
}
88+
s.Filters = append(s.Filters, func(s IShape) bool {
89+
if s.Data() != nil {
90+
return reflect.TypeOf(s.Data()).Implements(dataType)
91+
}
92+
return false
93+
})
94+
return s
95+
}
96+
7097
// Not adds a filter to the ShapeFilter that specifcally does not allow specified Shapes in.
7198
// The function returns the ShapeFiler for easy method chaining.
7299
func (s ShapeFilter) Not(shapes ...IShape) ShapeFilter {
@@ -86,18 +113,67 @@ func (s ShapeFilter) Shapes() ShapeCollection {
86113

87114
collection := ShapeCollection{}
88115

89-
s.operatingOn.ForEach(func(shape IShape) bool {
90-
for _, filter := range s.Filters {
91-
if filter(shape) {
92-
collection = append(collection, shape)
93-
}
94-
}
116+
s.ForEach(func(shape IShape) bool {
117+
collection = append(collection, shape)
95118
return true
96119
})
97120

98121
return collection
99122
}
100123

124+
// First returns the first shape that passes the ShapeFilter.
125+
func (s ShapeFilter) First() IShape {
126+
var returnShape IShape
127+
128+
s.ForEach(func(shape IShape) bool {
129+
returnShape = shape
130+
return false
131+
})
132+
133+
return returnShape
134+
}
135+
136+
// Last returns the last shape that passes the ShapeFilter (which means it has to step through all possible options before returning the last one).
137+
func (s ShapeFilter) Last() IShape {
138+
var returnShape IShape
139+
140+
s.ForEach(func(shape IShape) bool {
141+
returnShape = shape
142+
return true
143+
})
144+
145+
return returnShape
146+
}
147+
148+
// First returns the first shape in the ShapeCollection.
149+
func (s ShapeCollection) First() IShape {
150+
if len(s) > 0 {
151+
return s[0]
152+
}
153+
return nil
154+
}
155+
156+
// Last returns the last shape in the ShapeCollection.
157+
func (s ShapeCollection) Last() IShape {
158+
if len(s) > 0 {
159+
return s[len(s)-1]
160+
}
161+
return nil
162+
}
163+
164+
// Count returns the number of shapes that pass the filters as a ShapeCollection.
165+
func (s ShapeFilter) Count() int {
166+
167+
count := 0
168+
169+
s.ForEach(func(shape IShape) bool {
170+
count++
171+
return true
172+
})
173+
174+
return count
175+
}
176+
101177
// ShapeCollection is a slice of Shapes.
102178
type ShapeCollection []IShape
103179

@@ -123,3 +199,10 @@ func (s ShapeCollection) UnsetTags(tags Tags) {
123199
shape.Tags().Unset(tags)
124200
}
125201
}
202+
203+
// SortByDistance sorts the ShapeCollection by distance to the given point.
204+
func (s ShapeCollection) SortByDistance(point Vector) {
205+
sort.Slice(s, func(i, j int) bool {
206+
return s[i].Position().DistanceSquared(point) < s[j].Position().DistanceSquared(point)
207+
})
208+
}

space.go

+20-7
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ func (s *Space) RemoveAll() {
8585
}
8686

8787
// Shapes returns a new slice consisting of all of the shapes present in the Space.
88-
func (s *Space) Shapes() []IShape {
89-
return append(make([]IShape, 0, len(s.shapes)), s.shapes...)
88+
func (s *Space) Shapes() ShapeCollection {
89+
return append(make(ShapeCollection, 0, len(s.shapes)), s.shapes...)
9090
}
9191

92-
// ForEachShape iterates through each Object in the Space and runs the provided function on them, passing the Shape, its index in the
92+
// ForEachShape iterates through each shape in the Space and runs the provided function on them, passing the Shape, its index in the
9393
// Space's shapes slice, and the maximum number of shapes in the space.
9494
// If the function returns false, the iteration ends. If it returns true, it continues.
95-
func (s *Space) ForEachShape(forEach func(object IShape, index, maxCount int) bool) {
95+
func (s *Space) ForEachShape(forEach func(shape IShape, index, maxCount int) bool) {
9696

9797
for i, o := range s.shapes {
9898
if !forEach(o, i, len(s.shapes)) {
@@ -141,13 +141,26 @@ func (s *Space) Cell(cx, cy int) *Cell {
141141

142142
}
143143

144-
// Height returns the height of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a height of 15).
144+
// Width returns the spacial width of the Space grid in world coordinates.
145+
func (s *Space) Width() int {
146+
if len(s.cells) > 0 {
147+
return len(s.cells[0]) * s.cellWidth
148+
}
149+
return 0
150+
}
151+
152+
// Height returns the spacial height of the Space grid in world coordinates.
145153
func (s *Space) Height() int {
154+
return len(s.cells) * s.cellHeight
155+
}
156+
157+
// HeightInCells returns the height of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a HeightInCells() of 15).
158+
func (s *Space) HeightInCells() int {
146159
return len(s.cells)
147160
}
148161

149-
// Width returns the width of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a width of 20).
150-
func (s *Space) Width() int {
162+
// WidthInCells returns the width of the Space grid in Cells (so a 320x240 Space with 16x16 cells would have a WidthInCells() of 20).
163+
func (s *Space) WidthInCells() int {
151164
if len(s.cells) > 0 {
152165
return len(s.cells[0])
153166
}

utils.go

+25-7
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,22 @@ func (b Bounds) Height() float64 {
112112
return b.Max.Y - b.Min.Y
113113
}
114114

115+
// MaxAxis returns the maximum value out of either the Bounds's width or height.
116+
func (b Bounds) MaxAxis() float64 {
117+
if b.Width() > b.Height() {
118+
return b.Width()
119+
}
120+
return b.Height()
121+
}
122+
123+
// MinAxis returns the minimum value out of either the Bounds's width or height.
124+
func (b Bounds) MinAxis() float64 {
125+
if b.Width() > b.Height() {
126+
return b.Height()
127+
}
128+
return b.Width()
129+
}
130+
115131
// Move moves the Bounds, such that the center point is offset by {x, y}.
116132
func (b Bounds) Move(x, y float64) Bounds {
117133
b.Min.X += x
@@ -141,11 +157,11 @@ func (b Bounds) Intersection(other Bounds) Bounds {
141157
return overlap
142158
}
143159

144-
overlap.Min.X = math.Min(other.Max.X, b.Max.X)
145-
overlap.Max.X = math.Max(other.Min.X, b.Min.X)
160+
overlap.Max.X = math.Min(other.Max.X, b.Max.X)
161+
overlap.Min.X = math.Max(other.Min.X, b.Min.X)
146162

147-
overlap.Min.Y = math.Min(other.Max.Y, b.Max.Y)
148-
overlap.Max.Y = math.Max(other.Min.Y, b.Min.Y)
163+
overlap.Max.Y = math.Min(other.Max.Y, b.Max.Y)
164+
overlap.Min.Y = math.Max(other.Min.Y, b.Min.Y)
149165

150166
return overlap
151167

@@ -341,9 +357,11 @@ func LineTest(settings LineTestSettings) bool {
341357
})
342358

343359
// Loop through all intersections and iterate through them
344-
for i, c := range intersectionSets {
345-
if !settings.OnIntersect(c, i, len(intersectionSets)) {
346-
break
360+
if settings.OnIntersect != nil {
361+
for i, c := range intersectionSets {
362+
if !settings.OnIntersect(c, i, len(intersectionSets)) {
363+
break
364+
}
347365
}
348366
}
349367

0 commit comments

Comments
 (0)