engine refactor

This commit is contained in:
Anatoly Antonov 2026-05-23 00:55:35 +09:00
parent 9e663db9dc
commit 81b8e763bd
37 changed files with 3532 additions and 1639 deletions

View file

@ -1,40 +1,42 @@
package engine
// MaxAltitude triggers when altitude rises above Limit (in metres).
// Used as the burst condition for ascent stages.
type MaxAltitude struct {
import (
"fmt"
"math"
)
// Altitude triggers when the balloon altitude satisfies Op against Limit.
//
// Examples:
//
// Altitude{Op: OpGreaterEqual, Limit: 30000} — burst at 30 km
// Altitude{Op: OpLessEqual, Limit: 0} — sea-level descent termination
type Altitude struct {
Op Operator
Limit float64
On Action
}
func (c MaxAltitude) Name() string { return "max_altitude" }
func (c MaxAltitude) Violated(_ float64, s State) bool { return s.Altitude >= c.Limit }
func (c MaxAltitude) Action() Action { return c.On }
func (c Altitude) Name() string {
return fmt.Sprintf("altitude %s %g", c.Op, c.Limit)
}
func (c Altitude) Violated(_ float64, s State) bool { return c.Op.Test(s.Altitude, c.Limit) }
func (c Altitude) Action() Action { return c.On }
// MinAltitude triggers when altitude falls at or below Limit (in metres).
// With Limit=0 this is the "sea level" terminator.
type MinAltitude struct {
// Time triggers when the integration time t (UNIX seconds) satisfies Op
// against Limit.
type Time struct {
Op Operator
Limit float64
On Action
}
func (c MinAltitude) Name() string { return "min_altitude" }
func (c MinAltitude) Violated(_ float64, s State) bool { return s.Altitude <= c.Limit }
func (c MinAltitude) Action() Action { return c.On }
func (c Time) Name() string { return fmt.Sprintf("time %s %g", c.Op, c.Limit) }
func (c Time) Violated(t float64, _ State) bool { return c.Op.Test(t, c.Limit) }
func (c Time) Action() Action { return c.On }
// MaxTime triggers when t exceeds Limit (UNIX seconds). Used as a stop
// condition for float profiles.
type MaxTime struct {
Limit float64
On Action
}
func (c MaxTime) Name() string { return "max_time" }
func (c MaxTime) Violated(t float64, _ State) bool { return t > c.Limit }
func (c MaxTime) Action() Action { return c.On }
// TerrainContact triggers when altitude has dropped at or below ground level.
// Equivalent to Tawhiri's elevation termination.
// TerrainContact triggers when the ground elevation exceeds the balloon's
// altitude — i.e. the balloon has hit the ground.
type TerrainContact struct {
Provider TerrainProvider
On Action
@ -45,3 +47,103 @@ func (c TerrainContact) Violated(_ float64, s State) bool {
return c.Provider.Elevation(s.Lat, s.Lng) > s.Altitude
}
func (c TerrainContact) Action() Action { return c.On }
// PolygonMode selects whether Polygon fires when the balloon is inside or
// outside the configured polygon.
type PolygonMode int
const (
// PolygonInside fires when (lat, lng) lies inside the polygon — useful
// for "must not enter restricted airspace".
PolygonInside PolygonMode = iota
// PolygonOutside fires when (lat, lng) lies outside the polygon —
// useful for "must remain over the test range".
PolygonOutside
)
// PolygonVertex is one vertex of a geographic polygon. Latitudes are in
// degrees [-90, 90]; longitudes in degrees [0, 360) or [-180, 180]
// (callers normalise — see Polygon.Violated).
type PolygonVertex struct {
Lat float64
Lng float64
}
// Polygon is a constraint over a geographic polygon. The polygon is
// considered closed (last vertex connects to the first) and is interpreted
// in plate-carrée (rectangular lat/lng) coordinates with longitude
// wrap-around handling.
//
// Edges crossing the 180/-180 antimeridian are split via longitude
// normalisation against the polygon's centroid: callers that need
// great-circle accuracy should clip their polygon along the antimeridian
// before submitting.
type Polygon struct {
Vertices []PolygonVertex
Mode PolygonMode
On Action
// Label, if set, is returned by Name. Defaults to "polygon_inside" or
// "polygon_outside" based on Mode.
Label string
}
func (c Polygon) Name() string {
if c.Label != "" {
return c.Label
}
if c.Mode == PolygonOutside {
return "polygon_outside"
}
return "polygon_inside"
}
func (c Polygon) Action() Action { return c.On }
// Violated reports whether the state satisfies the polygon-containment rule.
func (c Polygon) Violated(_ float64, s State) bool {
if len(c.Vertices) < 3 {
return false
}
in := pointInPolygon(s.Lat, s.Lng, c.Vertices)
if c.Mode == PolygonInside {
return in
}
return !in
}
// pointInPolygon implements the ray-casting algorithm in lat/lng space.
//
// All vertices and the query point are normalised to within 180° of
// verts[0] before testing, so a polygon spanning the antimeridian is
// handled correctly as long as the polygon itself spans no more than 180°
// in longitude.
func pointInPolygon(lat, lng float64, verts []PolygonVertex) bool {
if len(verts) == 0 {
return false
}
ref := verts[0].Lng
qx := normLng(lng, ref)
inside := false
n := len(verts)
for i, j := 0, n-1; i < n; j, i = i, i+1 {
yi, yj := verts[i].Lat, verts[j].Lat
xi := normLng(verts[i].Lng, ref)
xj := normLng(verts[j].Lng, ref)
if (yi > lat) != (yj > lat) {
xIntersect := (xj-xi)*(lat-yi)/(yj-yi) + xi
if qx < xIntersect {
inside = !inside
}
}
}
return inside
}
// normLng rewrites v so that it lies within 180° of ref. With ref=10 and
// v=350, normLng returns -10.
func normLng(v, ref float64) float64 {
diff := math.Mod(v-ref+540, 360) - 180
return ref + diff
}