···33 SPDX-License-Identifier: ISC
44 ---------------------------------------------------------------------------*)
5566-(** Satellite state management for globe rendering.
77-88- Propagation-agnostic: takes a [propagator] function that maps dt → position.
99- The default constructor uses Kepler; [of_propagator] accepts any backend. *)
1010-116type propagator = dt:float -> Kepler.Vec3.t
127138type t = {
···1712 epoch_unix : float;
1813 ghost_points : Math.Vec3.t option array;
1914 trail_length : int;
2020- (* Kepler-specific, optional *)
2115 elements : Kepler.Analytic.elements option;
2216 pos0 : Kepler.Vec3.t;
2317 vel0 : Kepler.Vec3.t;
2418}
25192626-(* ── Safe GL coordinate conversion ─────────────────────────────────── *)
2020+(* ── Helpers ───────────────────────────────────────────────────────── *)
27212822let safe_gl (fallback : Math.Vec3.t) (p : Kepler.Vec3.t) =
2923 if Float.is_finite p.x && Float.is_finite p.y && Float.is_finite p.z then
3024 Gl_coord.of_kepler p
3125 else fallback
32263333-let make_ghost propagate ~period ~ghost_duration ~fallback =
2727+let make_ghost propagate ~dur ~fallback =
3428 let n = 120 in
3535- let dur =
3636- if Float.is_finite ghost_duration && ghost_duration > 0. then ghost_duration
3737- else if Float.is_finite period && period > 0. then period
3838- else 5400.
3939- in
4029 Array.init n (fun i ->
4130 let t = Float.of_int i *. dur /. Float.of_int n in
4231 Some (safe_gl fallback (propagate ~dt:t)))
43324444-(* ── Constructors ──────────────────────────────────────────────────── *)
3333+(* ── Propagator builders ───────────────────────────────────────────── *)
45344646-let v ~pos ~vel ~color ?(epoch_unix = 0.) ?(trail_length = 50) () =
4747- let elements = Kepler.Analytic.precompute ~pos ~vel in
4848- let period = Kepler.Analytic.period elements in
4949- let propagate ~dt = Kepler.Analytic.at_precomputed elements ~dt in
5050- let fallback = Gl_coord.of_kepler pos in
5151- let ghost_points =
5252- make_ghost propagate ~period ~ghost_duration:period ~fallback
5353- in
5454- {
5555- propagate;
5656- color;
5757- period;
5858- epoch_unix;
5959- ghost_points;
6060- trail_length;
6161- elements = Some elements;
6262- pos0 = pos;
6363- vel0 = vel;
6464- }
3535+let kepler ~pos ~vel =
3636+ let el = Kepler.Analytic.precompute ~pos ~vel in
3737+ fun ~dt -> Kepler.Analytic.at_precomputed el ~dt
3838+3939+(* ── Constructor ───────────────────────────────────────────────────── *)
65406666-let of_propagator ~propagate ~color ?(epoch_unix = 0.) ?(period = 5400.)
4141+let v ?propagate ?pos ?vel ~color ?(epoch_unix = 0.) ?(period = 0.)
6742 ?(ghost_duration = 0.) ?(trail_length = 50) () =
6868- let ghost_dur = if ghost_duration > 0. then ghost_duration else period in
6969- let fallback = safe_gl Math.Vec3.zero (propagate ~dt:0.) in
7070- let ghost_points =
7171- make_ghost propagate ~period ~ghost_duration:ghost_dur ~fallback
4343+ let propagate, elements, pos0, vel0, auto_period =
4444+ match (propagate, pos, vel) with
4545+ | Some p, _, _ ->
4646+ let pos0 = match pos with Some p -> p | None -> Kepler.Vec3.zero in
4747+ let vel0 = match vel with Some v -> v | None -> Kepler.Vec3.zero in
4848+ (p, None, pos0, vel0, 5400.)
4949+ | None, Some pos, Some vel ->
5050+ let el = Kepler.Analytic.precompute ~pos ~vel in
5151+ let p = Kepler.Analytic.period el in
5252+ let prop ~dt = Kepler.Analytic.at_precomputed el ~dt in
5353+ (prop, Some el, pos, vel, p)
5454+ | None, _, _ ->
5555+ let pos0 = match pos with Some p -> p | None -> Kepler.Vec3.zero in
5656+ ((fun ~dt:_ -> pos0), None, pos0, Kepler.Vec3.zero, 5400.)
5757+ in
5858+ let period =
5959+ if period > 0. then period
6060+ else if Float.is_finite auto_period && auto_period > 0. then auto_period
6161+ else 5400.
7262 in
6363+ let ghost_dur = if ghost_duration > 0. then ghost_duration else period in
6464+ let fallback = Gl_coord.of_kepler pos0 in
6565+ let ghost_points = make_ghost propagate ~dur:ghost_dur ~fallback in
7366 {
7467 propagate;
7568 color;
···7770 epoch_unix;
7871 ghost_points;
7972 trail_length;
8080- elements = None;
8181- pos0 = Kepler.Vec3.zero;
8282- vel0 = Kepler.Vec3.zero;
7373+ elements;
7474+ pos0;
7575+ vel0;
8376 }
84778578(* ── Accessors ─────────────────────────────────────────────────────── *)
···91849285(* ── Rendering ─────────────────────────────────────────────────────── *)
93869494-let position_at t ~dt =
9595- let fallback = Gl_coord.of_kepler t.pos0 in
9696- safe_gl fallback (t.propagate ~dt)
8787+let position_at t ~dt = safe_gl (Gl_coord.of_kepler t.pos0) (t.propagate ~dt)
97889889let trail_positions t ~dt =
9990 let n = t.trail_length in
···109100110101let dot t ~dt = (position_at t ~dt, t.color)
111102112112-(* ── Orbital elements (Kepler only) ───────────────────────────────── *)
103103+(* ── Orbital elements ──────────────────────────────────────────────── *)
113104114105let eccentricity t =
115106 match t.elements with
···117108 | None -> 0.
118109119110let inclination t =
120120- let hx = (t.pos0.y *. t.vel0.z) -. (t.pos0.z *. t.vel0.y) in
121121- let hy = (t.pos0.z *. t.vel0.x) -. (t.pos0.x *. t.vel0.z) in
122122- let hz = (t.pos0.x *. t.vel0.y) -. (t.pos0.y *. t.vel0.x) in
111111+ let p = t.pos0 and v = t.vel0 in
112112+ let hx = (p.y *. v.z) -. (p.z *. v.y) in
113113+ let hy = (p.z *. v.x) -. (p.x *. v.z) in
114114+ let hz = (p.x *. v.y) -. (p.y *. v.x) in
123115 let h_mag = sqrt ((hx *. hx) +. (hy *. hy) +. (hz *. hz)) in
124116 if h_mag < 1e-12 then 0.
125117 else acos (Float.max (-1.) (Float.min 1. (hz /. h_mag)))
+47-54
lib/satellite.mli
···11(** Satellite state management for globe rendering.
2233- Supports multiple propagation backends: Kepler (two-body), SGP4 (TLE), or
44- ephemeris interpolation (OEM). The propagation is abstracted behind a
55- [propagator] function — the rendering code doesn't know which method is
66- used.
77-88- {2 Propagation backends}
99-1010- {b Kepler} (default): Fast two-body analytical propagation from a J2000
1111- state vector. Accurate within ±1 orbit of epoch. Use for CDM visualization.
1212-1313- {b SGP4}: Standard TLE propagation with J2/drag. Accurate for days. Pass an
1414- SGP4 propagator via {!of_propagator}.
1515-1616- {b OEM}: Interpolate time-tagged ephemeris. Exact at provider precision.
1717- Pass an interpolation function via {!of_propagator}.
1818-1919- {2 Quick start}
33+ Supports multiple propagation backends via the optional [~propagate]
44+ parameter. Default: Kepler two-body from [~pos] and [~vel].
205216 {[
2222- (* From CDM state vector (Kepler): *)
2323- let sat = Satellite.v ~pos ~vel ~color ~epoch_unix ()
2424-2525- (* From custom propagator (SGP4, OEM, etc.): *)
2626- let sat =
2727- Satellite.of_propagator ~propagate ~color ~epoch_unix ~period
2828- ~ghost_duration ()
77+ (* Kepler (default — needs pos + vel): *)
88+ Satellite.v ~pos ~vel ~color ~epoch_unix ()
99+ (* SGP4 from TLE: *)
1010+ Satellite.v
1111+ ~propagate:(Satellite.sgp4 tle state)
1212+ ~color ~epoch_unix ~period ()
1313+ (* Custom propagator: *)
1414+ Satellite.v
1515+ ~propagate:(fun ~dt -> my_interp dt)
1616+ ~color ~epoch_unix ~period ()
2917 ]} *)
30183119type t
3220(** Satellite state with cached ghost orbit. *)
33213434-(** {1 Propagator type} *)
2222+type propagator = dt:float -> Kepler.Vec3.t
2323+(** Position at [dt] seconds from epoch, in J2000 km. *)
2424+2525+(** {1 Propagator builders} *)
35263636-type propagator = dt:float -> Kepler.Vec3.t
3737-(** A propagation function: given [dt] seconds from epoch, returns the J2000
3838- position in km. This is the abstraction that allows swapping Kepler, SGP4,
3939- or OEM interpolation. *)
2727+val kepler : pos:Kepler.Vec3.t -> vel:Kepler.Vec3.t -> propagator
2828+(** [kepler ~pos ~vel] builds a two-body Kepler propagator. Uses
2929+ {!Kepler.Analytic.precompute} internally — fast, ~20 FLOPs per call.
3030+ Accurate within ±1 orbit of epoch. *)
40314141-(** {1 Constructors} *)
3232+(** {1 Constructor} *)
42334334val v :
4444- pos:Kepler.Vec3.t ->
4545- vel:Kepler.Vec3.t ->
4646- color:Color.t ->
4747- ?epoch_unix:float ->
4848- ?trail_length:int ->
4949- unit ->
5050- t
5151-(** [v ~pos ~vel ~color ?epoch_unix ?trail_length ()] creates a satellite using
5252- Kepler (two-body) propagation from a J2000 state vector. *)
5353-5454-val of_propagator :
5555- propagate:propagator ->
3535+ ?propagate:propagator ->
3636+ ?pos:Kepler.Vec3.t ->
3737+ ?vel:Kepler.Vec3.t ->
5638 color:Color.t ->
5739 ?epoch_unix:float ->
5840 ?period:float ->
···6042 ?trail_length:int ->
6143 unit ->
6244 t
6363-(** [of_propagator ~propagate ~color ?epoch_unix ?period ?ghost_duration
6464- ?trail_length ()] creates a satellite from any propagation backend.
4545+(** [v ~color ()] creates a satellite.
65466666- - [propagate]: position at dt seconds from epoch (J2000 km)
6767- - [period]: orbital period in seconds (for ghost orbit and trail spacing;
6868- default 5400s)
6969- - [ghost_duration]: duration of ghost orbit arc in seconds (default: period)
4747+ Propagation is chosen by:
4848+ - [~propagate:p] — use propagator [p] (from {!kepler}, {!sgp4}, or custom)
4949+ - [~pos] + [~vel] without [~propagate] — auto-builds Kepler propagator
5050+ - [~propagate] without [~pos]/[~vel] — propagator-only (no orbital elements)
5151+5252+ Optional parameters:
5353+ - [period]: orbital period in seconds (auto-detected for Kepler/SGP4)
5454+ - [ghost_duration]: ghost orbit arc in seconds (default: [period])
7055 - [trail_length]: number of trail points (default 50) *)
71567257(** {1 Accessors} *)
73587459val color : t -> Color.t
6060+(** Satellite color. *)
6161+7562val epoch_unix : t -> float
6363+(** Unix timestamp of the propagation epoch. *)
6464+7665val period : t -> float
6666+(** Orbital period in seconds. *)
77677868(** {1 Rendering} *)
7969···8171(** Precomputed ghost orbit (GL coords). Computed once at creation. *)
82728373val position_at : t -> dt:float -> Math.Vec3.t
8484-(** GL position at [dt] seconds from epoch. *)
7474+(** [position_at t ~dt] returns the GL position at [dt] seconds from epoch. *)
85758676val trail_positions : t -> dt:float -> Math.Vec3.t option array
8787-(** Trail points for {!Globe_webgl.Orbit.add_trail}. *)
7777+(** Trail points for orbit rendering. *)
88788979val dot : t -> dt:float -> Math.Vec3.t * Color.t
9090-(** Current position + color for dot rendering. *)
8080+(** Current position and color for dot rendering. *)
91819292-(** {1 Orbital elements (Kepler only)} *)
8282+(** {1 Orbital elements (Kepler only, 0. otherwise)} *)
93839484val eccentricity : t -> float
9595-(** Eccentricity. Returns [0.] for non-Kepler propagators. *)
8585+(** [eccentricity t] returns the orbital eccentricity. Returns [0.] for
8686+ non-Kepler propagators. *)
96879788val inclination : t -> float
9898-(** Inclination in radians. Returns [0.] for non-Kepler propagators. *)
8989+(** [inclination t] returns the orbital inclination in radians. Returns [0.] for
9090+ non-Kepler propagators. *)
999110092val pp : t Fmt.t
9393+(** Pretty-print a satellite. *)
+4-4
lib/webgl/label.ml
···2121 container;
2222 { container; entries = [] }
23232424-let make_label text =
2424+let create text =
2525 let el = El.div [] in
2626 El.set_at (Jstr.of_string "style")
2727 (Some
···3737 match List.assoc_opt idx t.entries with
3838 | Some entry -> entry
3939 | None ->
4040- let el = make_label text in
4040+ let el = create text in
4141 El.append_children t.container [ el ];
4242 let entry = { el; visible = false } in
4343 t.entries <- (idx, entry) :: t.entries;
···6363 entry.visible <- false
6464 end
65656666-type label_info = { idx : int; text : string; pos : Globe.Math.Vec3.t }
6666+type info = { idx : int; text : string; pos : Globe.Math.Vec3.t }
67676868let update t ~projection ~view ~width ~height labels =
6969 (* Hide all first *)
7070 List.iter (fun (_, e) -> hide e) t.entries;
7171 (* Project and show visible labels *)
7272 List.iter
7373- (fun (info : label_info) ->
7373+ (fun (info : info) ->
7474 match
7575 Globe.Project.to_screen ~projection ~view ~width ~height info.pos
7676 with
+2-2
lib/webgl/label.mli
···1111(** [v container] creates a label manager inside [container]. The container
1212 should be a fixed-position overlay div. *)
13131414-type label_info = { idx : int; text : string; pos : Globe.Math.Vec3.t }
1414+type info = { idx : int; text : string; pos : Globe.Math.Vec3.t }
1515(** A label to display: satellite index, text, GL position. *)
16161717val update :
···2020 view:float array ->
2121 width:int ->
2222 height:int ->
2323- label_info list ->
2323+ info list ->
2424 unit
2525(** [update t ~projection ~view ~width ~height labels] positions label elements
2626 at projected screen coordinates. Labels not in the list are hidden. *)
+1-1
lib/webgl/shader.ml
···7788open Brr_canvas
991010-type shader_kind = Vertex | Fragment
1010+type kind = Vertex | Fragment
11111212let compile gl kind src =
1313 let typ =
+2-2
lib/webgl/shader.mli
···11(** GLSL shader compilation utilities and globe shader sources. *)
2233-type shader_kind = Vertex | Fragment (** The kind of GLSL shader. *)
33+type kind = Vertex | Fragment (** The kind of GLSL shader. *)
4455-val compile : Brr_canvas.Gl.t -> shader_kind -> string -> Brr_canvas.Gl.shader
55+val compile : Brr_canvas.Gl.t -> kind -> string -> Brr_canvas.Gl.shader
66(** [compile gl kind src] compiles a GLSL shader from source. *)
7788val program :
+1-2
test/test_project.ml
···8181 ]
8282 in
8383 let visible =
8484- Project.project_visible ~projection:proj ~view ~width:800 ~height:600
8585- positions
8484+ Project.visible ~projection:proj ~view ~width:800 ~height:600 positions
8685 in
8786 Alcotest.(check int) "2 of 3 visible" 2 (List.length visible);
8887 let indices = List.map (fun (i, _, _, _) -> i) visible in