diff --git a/alg/floatgeom/polygon_test.go b/alg/floatgeom/polygon_test.go index 86dd3c3b..1f379a7b 100644 --- a/alg/floatgeom/polygon_test.go +++ b/alg/floatgeom/polygon_test.go @@ -171,3 +171,13 @@ func BenchmarkPolygonConvexContains(b *testing.B) { benchContains = poly.ConvexContains(x, y) } } + +func Test_orient(t *testing.T) { + t.Parallel() + t.Run("3 empty points", func(t *testing.T) { + v := orient(Point2{}, Point2{}, Point2{}) + if v != 0 { + t.Fatal("expected orient to return 0 for 3 empty points") + } + }) +} diff --git a/alg/floatgeom/rect.go b/alg/floatgeom/rect.go index 0e75e593..b9b0f24b 100644 --- a/alg/floatgeom/rect.go +++ b/alg/floatgeom/rect.go @@ -1,5 +1,11 @@ package floatgeom +import ( + "math/rand" + + "github.com/oakmound/oak/v4/alg/span" +) + // A Rect2 represents a span from one point in 2D space to another. // If Min is less than max on any axis, it will return undefined results // for methods. @@ -349,3 +355,93 @@ func (r Rect3) ProjectZ() Rect2 { Max: r.Max.ProjectZ(), } } + +// MulConst multiplies the boundary points of this rectangle by i. +func (r Rect2) MulConst(i float64) Rect2 { + return Rect2{ + r.Min.MulConst(i), + r.Max.MulConst(i), + } +} + +// Poll returns a pseudorandom point from within this rectangle +func (r Rect2) Poll() Point2 { + return Point2{ + r.Min.X() + rand.Float64()*float64(r.W()), + r.Min.Y() + rand.Float64()*float64(r.H()), + } +} + +// Clamp returns a version of the provided point such that it is contained within r. If it was already contained in +// r, it will not be changed. +func (r Rect2) Clamp(pt Point2) Point2 { + for i := 0; i < r.MaxDimensions(); i++ { + if pt[i] < r.Min[i] { + pt[i] = r.Min[i] + } else if pt[i] > r.Max[i] { + pt[i] = r.Max[i] + } + } + return pt +} + +// Percentile returns a point within this rectangle along the vector from the top left to the bottom right of the +// rectangle, where for example, 0.0 will be r.Min, 1.0 will be r.Max, and 2.0 will be project the vector beyond r +// and return r.Min + {r.W()*2, r.H()*2} +func (r Rect2) Percentile(f float64) Point2 { + return Point2{ + r.Min.X() + f*float64(r.W()), + r.Min.Y() + f*float64(r.H()), + } +} + +// MulSpan returns this rectangle as a Point2 Span after multiplying the boundary points of the rectangle by f. +func (r Rect2) MulSpan(f float64) span.Span[Point2] { + return r.MulConst(f) +} + +// MulConst multiplies the boundary points of this rectangle by i. +func (r Rect3) MulConst(i float64) Rect3 { + return Rect3{ + r.Min.MulConst(i), + r.Max.MulConst(i), + } +} + +// Poll returns a pseudorandom point from within this rectangle +func (r Rect3) Poll() Point3 { + return Point3{ + r.Min.X() + (rand.Float64() * float64(r.W())), + r.Min.Y() + (rand.Float64() * float64(r.H())), + r.Min.Z() + (rand.Float64() * float64(r.D())), + } +} + +// Clamp returns a version of the provided point such that it is contained within r. If it was already contained in +// r, it will not be changed. +func (r Rect3) Clamp(pt Point3) Point3 { + for i := 0; i < r.MaxDimensions(); i++ { + if pt[i] < r.Min[i] { + pt[i] = r.Min[i] + } else if pt[i] > r.Max[i] { + pt[i] = r.Max[i] + } + } + return pt +} + +// Percentile returns a point within this rectangle along the vector from the top left to the bottom right of the +// rectangle, where for example, 0.0 will be r.Min, 1.0 will be r.Max, and 2.0 will be project the vector beyond r +// and return r.Min + {r.W()*2, r.H()*2, r.D()*2} +func (r Rect3) Percentile(f float64) Point3 { + return Point3{ + r.Min.X() + (f * float64(r.W())), + r.Min.Y() + (f * float64(r.H())), + r.Min.Z() + (f * float64(r.D())), + } +} + +// MulConst multiplies the boundary points of this rectangle by i. +func (r Rect3) MulSpan(f float64) span.Span[Point3] { + return r.MulConst(f) +} diff --git a/alg/floatgeom/rect_test.go b/alg/floatgeom/rect_test.go index 75b95a63..ecc94479 100644 --- a/alg/floatgeom/rect_test.go +++ b/alg/floatgeom/rect_test.go @@ -269,3 +269,67 @@ func projectZHolds(x1, y1, z1, x2, y2, z2 float64) bool { projected := r.ProjectZ() return expected == projected } + +func TestRect2Span(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + r := NewRect2WH(1, 1, 9, 9) + p1 := r.Percentile(1.0) + if p1 != r.Max { + t.Errorf("Percentile(1.0) did not return max point: got %v expected %v", p1, r.Max) + } + p2 := r.Percentile(0.0) + if p2 != r.Min { + t.Errorf("Percentile(0.0) did not return min point: got %v expected %v", p2, r.Min) + } + const pollTries = 100 + for i := 0; i < pollTries; i++ { + if !r.Contains(r.Poll()) { + t.Fatalf("polled point did not lie within the creating rectangle") + } + } + p3 := r.Clamp(Point2{0, 5}) + if p3 != (Point2{1, 5}) { + t.Errorf("Clamp(0,5) did not return {1,5}: got %v", p3) + } + p4 := r.Clamp(Point2{2, 11}) + if p4 != (Point2{2, 10}) { + t.Errorf("Clamp(2,11) did not return {2,10}: got %v", p4) + } + r2 := r.MulSpan(4) + if r2 != NewRect2(4, 4, 40, 40) { + t.Errorf("MulSpan did not return {4,4,40,40}: got %v", r2) + } + }) +} + +func TestRect3Span(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + r := NewRect3WH(1, 1, 1, 9, 9, 9) + p1 := r.Percentile(1.0) + if p1 != r.Max { + t.Errorf("Percentile(1.0) did not return max point: got %v expected %v", p1, r.Max) + } + p2 := r.Percentile(0.0) + if p2 != r.Min { + t.Errorf("Percentile(0.0) did not return min point: got %v expected %v", p2, r.Min) + } + const pollTries = 100 + for i := 0; i < pollTries; i++ { + if !r.Contains(r.Poll()) { + t.Fatalf("polled point did not lie within the creating rectangle") + } + } + p3 := r.Clamp(Point3{0, -1, 5}) + if p3 != (Point3{1, 1, 5}) { + t.Errorf("Clamp(0,-1,5) did not return {1,1,5}: got %v", p3) + } + p4 := r.Clamp(Point3{20, 2, 11}) + if p4 != (Point3{10, 2, 10}) { + t.Errorf("Clamp(20, 2,11) did not return {10,2,10}: got %v", p4) + } + r2 := r.MulSpan(4) + if r2 != NewRect3(4, 4, 4, 40, 40, 40) { + t.Errorf("MulSpan did not return {4,4,4,40,40,40}: got %v", r2) + } + }) +} diff --git a/entities/entity.go b/entities/entity.go index 02e14106..00ccc578 100644 --- a/entities/entity.go +++ b/entities/entity.go @@ -31,7 +31,8 @@ type Generator struct { UseMouseTree bool WithoutCollision bool - Children [][]Option + Children [][]Option + ExplicitChildren []*Entity } func And(opts ...Option) Option { @@ -50,6 +51,13 @@ func WithChild(opts ...Option) Option { } } +func WithExplicitChild(e *Entity) Option { + return func(s Generator) Generator { + s.ExplicitChildren = append(s.ExplicitChildren, e) + return s + } +} + func WithRect(v floatgeom.Rect2) Option { return func(s Generator) Generator { s.Position = v.Min @@ -135,6 +143,9 @@ func (e *Entity) Shift(delta floatgeom.Point2) { e.X(), e.Y(), e.W(), e.H(), e.Space, ) } + for _, c := range e.Children { + c.Shift(delta) + } } func (e *Entity) SetX(x float64) { @@ -153,6 +164,9 @@ func (e *Entity) ShiftX(x float64) { e.X(), e.Y(), e.W(), e.H(), e.Space, ) } + for _, c := range e.Children { + c.ShiftX(x) + } } func (e *Entity) ShiftY(y float64) { @@ -163,23 +177,17 @@ func (e *Entity) ShiftY(y float64) { e.X(), e.Y(), e.W(), e.H(), e.Space, ) } + for _, c := range e.Children { + c.ShiftY(y) + } } func (e *Entity) SetPos(p floatgeom.Point2) { - w, h := e.W(), e.H() - e.Rect = floatgeom.NewRect2WH(p.X(), p.Y(), w, h) - e.Renderable.SetPos(p.X(), p.Y()) - if e.Tree != nil { - e.Tree.UpdateSpace( - e.X(), e.Y(), e.W(), e.H(), e.Space, - ) - } + e.Shift(p.Sub(e.Rect.Min)) } -// TODO: take a point, not floats func (e *Entity) ShiftPos(x, y float64) { - p := e.Rect.Min - e.SetPos(p.Add(floatgeom.Point2{x, y})) + e.Shift(floatgeom.Point2{x, y}) } func (e *Entity) HitLabel(label collision.Label) *collision.Space { @@ -214,11 +222,16 @@ func New(ctx *scene.Context, opts ...Option) *Entity { g = o(g) } - children := make([]*Entity, len(g.Children)) + children := make([]*Entity, len(g.Children)+len(g.ExplicitChildren)) for i, childOpts := range g.Children { childOpts = append(childOpts, WithOffset(g.Position)) children[i] = New(ctx, childOpts...) } + for i, explicitChild := range g.ExplicitChildren { + child := explicitChild + child.ShiftPos(g.Position.X(), g.Position.Y()) + children[i+len(g.Children)] = child + } e := &Entity{ ctx: ctx, @@ -240,8 +253,9 @@ func New(ctx *scene.Context, opts ...Option) *Entity { if m, isMod := e.Renderable.(render.Modifiable); g.Mod != nil && isMod { e.Renderable = m.Modify(g.Mod) } - - e.Renderable.SetPos(e.X(), e.Y()) + if e.Renderable != nil { + e.Renderable.SetPos(e.X(), e.Y()) + } if g.Parent == nil { cid := ctx.CallerMap.Register(e) @@ -265,7 +279,7 @@ func New(ctx *scene.Context, opts ...Option) *Entity { e.Tree.Add(e.Space) } - if len(g.DrawLayers) != 0 { + if len(g.DrawLayers) != 0 && e.Renderable != nil { ctx.Draw(e.Renderable, g.DrawLayers...) } diff --git a/render/particle/generator.go b/render/particle/generator.go index ffb84c57..52213582 100644 --- a/render/particle/generator.go +++ b/render/particle/generator.go @@ -5,6 +5,7 @@ import ( "github.com/oakmound/oak/v4/alg/span" "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" ) var ( @@ -29,6 +30,7 @@ type Generator interface { // Modeled after Parcycle type BaseGenerator struct { physics.Vector + DrawStack *render.DrawStack // This float is currently forced to an integer // at new particle rotation. This should be changed // to something along the lines of 'new per 30 frames', @@ -68,6 +70,7 @@ func (bg *BaseGenerator) GetBaseGenerator() *BaseGenerator { func (bg *BaseGenerator) setDefaults() { *bg = BaseGenerator{ Vector: physics.NewVector(0, 0), + DrawStack: nil, NewPerFrame: span.NewConstant(1.0), LifeSpan: span.NewConstant(60.0), Angle: span.NewConstant(0.0), diff --git a/render/particle/options.go b/render/particle/options.go index d822f134..195be7c2 100644 --- a/render/particle/options.go +++ b/render/particle/options.go @@ -6,6 +6,7 @@ import ( "github.com/oakmound/oak/v4/alg" "github.com/oakmound/oak/v4/alg/span" "github.com/oakmound/oak/v4/physics" + "github.com/oakmound/oak/v4/render" ) // And chains together particle options into a single option @@ -116,3 +117,10 @@ func Limit(limit int) func(Generator) { g.GetBaseGenerator().ParticleLimit = limit } } + +// DrawStack sets the current drawstack so that we dont use the globaldrawstack +func DrawStack(drawStack *render.DrawStack) func(Generator) { + return func(g Generator) { + g.GetBaseGenerator().DrawStack = drawStack + } +} diff --git a/render/particle/source.go b/render/particle/source.go index 601e6cd7..d0a859ca 100644 --- a/render/particle/source.go +++ b/render/particle/source.go @@ -177,7 +177,11 @@ func (ps *Source) addParticles() { ps.particles[ps.nextPID] = p ps.nextPID++ p.SetLayer(ps.Layer(bp.GetPos())) - render.Draw(p, ps.stackLevel) + if pg.DrawStack == nil { + render.Draw(p, ps.stackLevel) + } else { + pg.DrawStack.Draw(p, ps.stackLevel) + } } }