Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 24 additions & 7 deletions pkg/plugin/physics2d/component/collider.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ const (
ShapeTypeBox
// ShapeTypeConvexPolygon uses Vertices as a convex polygon in the shape's local frame.
ShapeTypeConvexPolygon
// ShapeTypeStaticChain uses ChainPoints for open chain segments (static/terrain-style
// geometry only; not for moving dynamic shapes).
// ShapeTypeStaticChain uses ChainPoints for open chain segments (static or kinematic
// bodies only; not for dynamic bodies which require mass).
ShapeTypeStaticChain
// ShapeTypeStaticChainLoop uses ChainPoints for closed chain loops (static or kinematic
// bodies only; not for dynamic bodies). Unlike ShapeTypeStaticChain, the last vertex
// automatically connects back to the first, creating a sealed boundary.
ShapeTypeStaticChainLoop
// ShapeTypeEdge uses EdgeVertices (exactly 2 points) for a single line segment
// (static or kinematic bodies only). Lighter than a 2-point chain for isolated barriers
// or triggers.
ShapeTypeEdge
)

// ColliderShape is one child shape inside a compound Collider2D.
Expand All @@ -33,17 +41,20 @@ const (
// - ShapeTypeBox → HalfExtents (half-width on X, half-height on Y, axis-aligned before LocalOffset/LocalRotation)
// - ShapeTypeConvexPolygon → Vertices (convex polygon, respect backend limits)
// - ShapeTypeStaticChain → ChainPoints (open polyline in local space)
// - ShapeTypeStaticChainLoop → ChainPoints (closed loop in local space)
// - ShapeTypeEdge → EdgeVertices (exactly 2 points in local space)
type ColliderShape struct {
ShapeType ShapeType `json:"shape_type"`
LocalOffset Vec2 `json:"local_offset"`
LocalRotation float64 `json:"local_rotation"`
IsSensor bool `json:"is_sensor"`

// Geometry (use fields matching ShapeType).
Radius float64 `json:"radius,omitempty"`
HalfExtents Vec2 `json:"half_extents,omitempty"`
Vertices []Vec2 `json:"vertices,omitempty"`
ChainPoints []Vec2 `json:"chain_points,omitempty"`
Radius float64 `json:"radius,omitempty"`
HalfExtents Vec2 `json:"half_extents,omitempty"`
Vertices []Vec2 `json:"vertices,omitempty"`
ChainPoints []Vec2 `json:"chain_points,omitempty"`
EdgeVertices [2]Vec2 `json:"edge_vertices,omitempty"`

// Material and per-shape collision filtering (fixture-level in Box2D).
Friction float64 `json:"friction"`
Expand Down Expand Up @@ -117,9 +128,15 @@ func (s ColliderShape) Validate() error {
return err
}
}
for i, v := range s.EdgeVertices {
if err := validateVec2(fmt.Sprintf("edge_vertices[%d]", i), v); err != nil {
return err
}
}

switch s.ShapeType {
case ShapeTypeCircle, ShapeTypeBox, ShapeTypeConvexPolygon, ShapeTypeStaticChain:
case ShapeTypeCircle, ShapeTypeBox, ShapeTypeConvexPolygon, ShapeTypeStaticChain,
ShapeTypeStaticChainLoop, ShapeTypeEdge:
default:
return fmt.Errorf("shape_type: unknown value %d", s.ShapeType)
}
Expand Down
30 changes: 28 additions & 2 deletions pkg/plugin/physics2d/internal/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,12 @@ func attachFixture(
sh component.ColliderShape,
bodyType uint8,
) error {
if sh.ShapeType == component.ShapeTypeStaticChain && bodyType != box2d.B2BodyType.B2_staticBody {
return errors.New("static chain colliders require a static body")
//nolint:exhaustive // We only care about static chain, static chain loop, and edge shapes
switch sh.ShapeType {
case component.ShapeTypeStaticChain, component.ShapeTypeStaticChainLoop, component.ShapeTypeEdge:
if bodyType == box2d.B2BodyType.B2_dynamicBody {
return fmt.Errorf("%d shape type cannot be used on dynamic bodies (zero mass)", sh.ShapeType)
}
}

shape, err := buildShape(sh)
Expand Down Expand Up @@ -155,6 +159,10 @@ func buildShape(sh component.ColliderShape) (box2d.B2ShapeInterface, error) {
return buildPolygonShape(sh)
case component.ShapeTypeStaticChain:
return buildChainShape(sh)
case component.ShapeTypeStaticChainLoop:
return buildChainLoopShape(sh)
case component.ShapeTypeEdge:
return buildEdgeShape(sh)
default:
return nil, fmt.Errorf("unknown shape_type %d", sh.ShapeType)
}
Expand Down Expand Up @@ -194,6 +202,24 @@ func buildChainShape(sh component.ColliderShape) (box2d.B2ShapeInterface, error)
return &chain, nil
}

func buildChainLoopShape(sh component.ColliderShape) (box2d.B2ShapeInterface, error) {
pts := make([]box2d.B2Vec2, len(sh.ChainPoints))
for i := range sh.ChainPoints {
pts[i] = shapePointToBodySpace(sh.ChainPoints[i], sh.LocalOffset, sh.LocalRotation)
}
chain := box2d.MakeB2ChainShape()
chain.CreateLoop(pts, len(sh.ChainPoints))
return &chain, nil
}

func buildEdgeShape(sh component.ColliderShape) (box2d.B2ShapeInterface, error) {
v1 := shapePointToBodySpace(sh.EdgeVertices[0], sh.LocalOffset, sh.LocalRotation)
v2 := shapePointToBodySpace(sh.EdgeVertices[1], sh.LocalOffset, sh.LocalRotation)
edge := box2d.MakeB2EdgeShape()
edge.Set(v1, v2)
return &edge, nil
}

// shapePointToBodySpace maps a point from shape-local space into body-local space using
// LocalOffset and LocalRotation (radians, CCW +Y up) on the ColliderShape.
func shapePointToBodySpace(p, offset component.Vec2, localRot float64) box2d.B2Vec2 {
Expand Down
8 changes: 6 additions & 2 deletions pkg/plugin/physics2d/internal/shadow.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func deepCopyColliderShape(s component.ColliderShape) component.ColliderShape {
HalfExtents: s.HalfExtents,
Vertices: cloneVec2Slice(s.Vertices),
ChainPoints: cloneVec2Slice(s.ChainPoints),
EdgeVertices: s.EdgeVertices,
Friction: s.Friction,
Restitution: s.Restitution,
Density: s.Density,
Expand Down Expand Up @@ -140,7 +141,9 @@ func colliderShapeDeepEqual(a, b component.ColliderShape) bool {
a.GroupIndex != b.GroupIndex {
return false
}
return vec2SliceEqual(a.Vertices, b.Vertices) && vec2SliceEqual(a.ChainPoints, b.ChainPoints)
return vec2SliceEqual(a.Vertices, b.Vertices) &&
vec2SliceEqual(a.ChainPoints, b.ChainPoints) &&
a.EdgeVertices == b.EdgeVertices
}

// Collider2DStructuralEqual reports whether two colliders match for Box2D fixture shape
Expand All @@ -166,7 +169,8 @@ func colliderShapeStructuralEqual(a, b component.ColliderShape) bool {
a.Radius == b.Radius &&
vec2Equal(a.HalfExtents, b.HalfExtents) &&
vec2SliceEqual(a.Vertices, b.Vertices) &&
vec2SliceEqual(a.ChainPoints, b.ChainPoints)
vec2SliceEqual(a.ChainPoints, b.ChainPoints) &&
a.EdgeVertices == b.EdgeVertices
}

// ColliderShapeMutableFieldsEqual compares per-shape fields that Box2D can update without
Expand Down
10 changes: 6 additions & 4 deletions pkg/plugin/physics2d/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ const (

// Collider shape kinds (ColliderShape).
const (
ShapeTypeCircle = component.ShapeTypeCircle
ShapeTypeBox = component.ShapeTypeBox
ShapeTypeConvexPolygon = component.ShapeTypeConvexPolygon
ShapeTypeStaticChain = component.ShapeTypeStaticChain
ShapeTypeCircle = component.ShapeTypeCircle
ShapeTypeBox = component.ShapeTypeBox
ShapeTypeConvexPolygon = component.ShapeTypeConvexPolygon
ShapeTypeStaticChain = component.ShapeTypeStaticChain
ShapeTypeStaticChainLoop = component.ShapeTypeStaticChainLoop
ShapeTypeEdge = component.ShapeTypeEdge
)

// Contact / trigger system events (implement ecs.SystemEvent; register with WithSystemEventEmitter).
Expand Down
Loading