Added terminology at the top of file for clarity in comments explaining things. Added Null geometry option instead of a panic. The center of mass and orientation are now stored as rotors, not generators; changed draw and update code accordingly. Added a line that points straight ahead to orient player. Fixed acceleration logic error that didn't map it to world coordinates. Added comments, probably more than needed. Probably should have broken this up more as I went.

This commit is contained in:
2025-08-23 17:52:13 -05:00
parent 0f351593ab
commit 2b26cd4197

View File

@@ -1,3 +1,22 @@
// Some notes on terminology as used in this project:
//
// Body frame (or body-fixed frame):
// - A coordinate frame that moves and rotates with the body.
// - The origin of the body is fixed at the origin of this frame, and points on the body
// have constant coordinates relative to the origin. For example, the ship's nose
// is always at a fixed distance along the x-axis in this frame.
//
// Local frame (or co-moving frame):
// - A coordinate frame that moves with the object's position (e.g., center of mass)
// but does not rotate with it.
// - Points on the body may rotate relative to this frame as the object rotates in space.
//
// World frame (or inertial frame):
// - A fixed coordinate frame that does not move or rotate with the body.
// - All positions and orientations of the object are expressed relative to this absolute frame.
//
// This should be consistent with standard terminology.
use std::f64; use std::f64;
use std::collections::HashSet; use std::collections::HashSet;
@@ -11,7 +30,7 @@ use cga::CGA;
const PI: f64 = f64::consts::PI; const PI: f64 = f64::consts::PI;
const TWO_PI: f64 = 2.*PI; const TWO_PI: f64 = 2.*PI;
const ERROR: f64 = 0.000001; const ERROR: f64 = 0.0001;
const WIDTH: f64 = 600.; const WIDTH: f64 = 600.;
const HEIGHT: f64 = 600.; const HEIGHT: f64 = 600.;
@@ -62,15 +81,16 @@ const DRAG: f64 = 1.;
const ACCEL_STR: f64 = 1.; const ACCEL_STR: f64 = 1.;
struct Ship { struct Ship {
com: CGA, com_rotor: CGA,
verts: (CGA, CGA, CGA), verts: (CGA, CGA, CGA),
vel: CGA, vel: CGA,
orientation: CGA, orientation: CGA,
} }
enum _Geometry { enum Geometry {
Circle(f64, f64, f64), //x,y,r Circle(f64, f64, f64), //x,y,r
Line(f64, f64, f64) //x,r,theta Line(f64, f64, f64), //x,r,theta
Null,
} }
/* Translation of a Euclidean Point. Need to use hyperbolic*/ /* Translation of a Euclidean Point. Need to use hyperbolic*/
@@ -141,11 +161,11 @@ fn get_hyperbolic_point(point: &CGA) -> (f64, f64) {
(r*x,r*y) (r*x,r*y)
} }
fn _get_geometry(circle: &CGA) -> _Geometry { fn get_geometry(circle: &CGA) -> Geometry {
//Naively assume we are given a valid line/circle //Naively assume we are given a valid line/circle
let n_bar = CGA::e4() - CGA::e5(); let n_bar = CGA::e4() - CGA::e5();
let n_vec = CGA::e4() + CGA::e5(); let n_vec = CGA::e4() + CGA::e5();
let dual = circle.Dual(); let dual = (&circle).Dual();
let l = (&dual | &n_vec)[SCALAR]; let l = (&dual | &n_vec)[SCALAR];
let c = (&dual | &n_bar)[SCALAR]; let c = (&dual | &n_bar)[SCALAR];
let a = (&dual | CGA::e1())[SCALAR]; let a = (&dual | CGA::e1())[SCALAR];
@@ -155,34 +175,34 @@ fn _get_geometry(circle: &CGA) -> _Geometry {
//LINE //LINE
let theta = (-a).atan2(b); let theta = (-a).atan2(b);
let norm_sqr = a*a + b*b; let norm_sqr = a*a + b*b;
if norm_sqr < ERROR { if norm_sqr < ERROR*ERROR {
panic!("Not a line or circle"); return Geometry::Null;
} }
let x = a*c/norm_sqr/2.; let x = a*c/norm_sqr/2.;
let y = b*c/norm_sqr/2.; let y = b*c/norm_sqr/2.;
return _Geometry::Line(x,y,theta); return Geometry::Line(x,y,theta);
} else { } else {
//Circle //Circle
let c_x = a/l; let c_x = a/l;
let c_y = b/l; let c_y = b/l;
let r_sqr = c_x*c_x + c_y*c_y + c/l; let r_sqr = c_x*c_x + c_y*c_y + c/l;
let r = r_sqr.sqrt(); let r = r_sqr.sqrt();
return _Geometry::Circle(-c_x, -c_y, r); return Geometry::Circle(-c_x, -c_y, r);
} }
} }
fn _draw_line(context: &CanvasRenderingContext2d, line: &CGA) { fn draw_line(context: &CanvasRenderingContext2d, line: &CGA) {
context.begin_path(); context.begin_path();
context.set_stroke_style_str(_GREEN); context.set_stroke_style_str(_GREEN);
let geometry = _get_geometry(line); let geometry = get_geometry(line);
match geometry { match geometry {
_Geometry::Circle(x,y,r) => { Geometry::Circle(x,y,r) => {
let (canvas_x, canvas_y) = point_to_screen_space(x, y); let (canvas_x, canvas_y) = point_to_screen_space(x, y);
let canvas_r = r*RADIUS; let canvas_r = r*RADIUS;
context.arc(canvas_x, canvas_y, canvas_r, 0., TWO_PI).unwrap(); context.arc(canvas_x, canvas_y, canvas_r, 0., TWO_PI).unwrap();
}, },
_Geometry::Line(x,y,theta) => { Geometry::Line(x,y,theta) => {
let (p1x, p1y) = (x - theta.cos(), y - theta.sin()); let (p1x, p1y) = (x - theta.cos(), y - theta.sin());
let (p2x, p2y) = (x + theta.cos(), y + theta.sin()); let (p2x, p2y) = (x + theta.cos(), y + theta.sin());
let (canvas_x1, canvas_y1) = point_to_screen_space(p1x, p1y); let (canvas_x1, canvas_y1) = point_to_screen_space(p1x, p1y);
@@ -191,6 +211,7 @@ fn _draw_line(context: &CanvasRenderingContext2d, line: &CGA) {
context.move_to(canvas_x1, canvas_y1); context.move_to(canvas_x1, canvas_y1);
context.line_to(canvas_x2, canvas_y2); context.line_to(canvas_x2, canvas_y2);
}, },
Geometry::Null => {},
} }
context.stroke(); context.stroke();
} }
@@ -275,16 +296,16 @@ impl Game {
let ty_gen = CGA::e25(); let ty_gen = CGA::e25();
let ship = Ship { let ship = Ship {
com: CGA::zero(), com_rotor: CGA::new(1., SCALAR),
verts: (-0.1*&tx_gen, 0.05*&tx_gen+0.02*&ty_gen, 0.05*&tx_gen-0.02*ty_gen), verts: (-0.1*&tx_gen, 0.05*&tx_gen+0.02*&ty_gen, 0.05*&tx_gen-0.02*ty_gen),
vel: CGA::zero(), vel: CGA::zero(),
orientation: CGA::zero() orientation: CGA::new(1., SCALAR),
}; };
Game { Game {
context: context, context: context,
ship, ship,
keys: HashSet::new() keys: HashSet::new(),
} }
} }
@@ -297,26 +318,27 @@ impl Game {
self.context.arc(CENTER_X, CENTER_Y, RADIUS, 0., TWO_PI).unwrap(); self.context.arc(CENTER_X, CENTER_Y, RADIUS, 0., TWO_PI).unwrap();
self.context.stroke(); self.context.stroke();
let origin = gen_hyperbolic_point(&CGA::zero()); let origin = gen_hyperbolic_point(&CGA::zero()); //Origin in the body frame.
let com = bivector_exponential(&self.ship.com); let orient_rotor = &self.ship.orientation; //Transforms body frame to local frame.
let orient_rotor = bivector_exponential(&(0.5 * &self.ship.orientation)); let com = &self.ship.com_rotor; //Transforms local frame to world frame.
let v0_rotor = &com*&orient_rotor*bivector_exponential(&self.ship.verts.0); let v0_rotor = com*orient_rotor*bivector_exponential(&self.ship.verts.0); //Combined transformation to locate a vertex of the ship in the world frame.
let v1_rotor = &com*&orient_rotor*bivector_exponential(&self.ship.verts.1); let v1_rotor = com*orient_rotor*bivector_exponential(&self.ship.verts.1); //Combined transformation ...
let v2_rotor = &com*&orient_rotor*bivector_exponential(&self.ship.verts.2); let v2_rotor = com*orient_rotor*bivector_exponential(&self.ship.verts.2); //Combined transformation ...
let p0 = &v0_rotor*&origin*&v0_rotor.Reverse(); let p0 = &v0_rotor*&origin*&v0_rotor.Reverse(); //Produce a vertex of the ship in the world frame.
let p1 = &v1_rotor*&origin*&v1_rotor.Reverse(); let p1 = &v1_rotor*&origin*&v1_rotor.Reverse(); //...
let p2 = &v2_rotor*&origin*&v2_rotor.Reverse(); let p2 = &v2_rotor*&origin*&v2_rotor.Reverse(); //...
draw_point(&self.context, &(&com*&origin*&com.Reverse())); draw_point(&self.context, &(com*&origin*com.Reverse()));
draw_point(&self.context, &p0); draw_point(&self.context, &p0);
draw_point(&self.context, &p1); draw_point(&self.context, &p1);
draw_point(&self.context, &p2); draw_point(&self.context, &p2);
//self.context.begin_path(); //Construct and draw line pointing straight ahead, to help orient the player.
//self.context.set_fill_style_str(GREEN); let com_p = com * gen_hyperbolic_point(&CGA::zero()) * com.Reverse();
//self.context.arc(x, y, 20., 0., TWO_PI).unwrap(); let delta_gen = com * orient_rotor * CGA::e14() * &orient_rotor.Reverse() * com.Reverse();
//self.context.fill(); let dot = delta_gen | &com_p;
let line = CGA::e4() ^ com_p ^ dot ^ CGA::e3();
draw_line(&self.context, &line);
} }
pub fn update(&mut self, dt_m: f64) { pub fn update(&mut self, dt_m: f64) {
@@ -325,32 +347,35 @@ impl Game {
} else { } else {
dt_m / 1000. dt_m / 1000.
}; };
let vel = self.ship.vel.clone(); let vel = self.ship.vel.clone(); //Velocity at beginning of frame
let mut vel_2 = self.ship.vel.clone(); let mut vel_2 = self.ship.vel.clone();
let mut a_vec = - DRAG * &vel_2; let mut a_vec = - DRAG * &vel_2;
if self.keys.contains("w") { if self.keys.contains("w") {
let orient = bivector_exponential(&(0.5*&self.ship.orientation)); let orient = &self.ship.orientation; //Transforms body frame to local frame
let accel = &orient * ACCEL_STR * CGA::e15() * orient.Reverse(); let pos = &self.ship.com_rotor; //Transforms local frame to world frame
a_vec = a_vec - accel; let accel_body = ACCEL_STR * CGA::e15(); //The acceleration in the body frame.
let accel_local = orient * accel_body * orient.Reverse(); //The acceleration in the local frame.
let accel_world = pos * accel_local * pos.Reverse(); //The acceleration in the world frame.
a_vec = a_vec - accel_world; //Sum of drag and thrust.
} }
if self.keys.contains("q") { if self.keys.contains("q") {
self.ship.orientation = &self.ship.orientation + 1. * dt_s * CGA::e12(); self.ship.orientation = bivector_exponential(&(dt_s * CGA::e12())) * &self.ship.orientation;
} }
if self.keys.contains("e") { if self.keys.contains("e") {
self.ship.orientation = &self.ship.orientation - 1. * dt_s * CGA::e12(); self.ship.orientation = bivector_exponential(&(-dt_s * CGA::e12())) * &self.ship.orientation;
} }
vel_2 = vel_2 + a_vec*dt_s; vel_2 = vel_2 + a_vec*dt_s; //Velocity at end of frame
let vel_3 = 0.5*(vel+&vel_2); let vel_3 = 0.5*(vel+&vel_2);
let delta = vel_3 * dt_s; let delta = vel_3 * dt_s;
let pos = &self.ship.com + delta; let pos = bivector_exponential(&delta)*&self.ship.com_rotor; //Update position.
self.ship.vel = vel_2; self.ship.vel = vel_2;
self.ship.com = pos; self.ship.com_rotor = pos;
} }
pub fn key_down(&mut self, key: String) { pub fn key_down(&mut self, key: String) {