initial version
This commit is contained in:
commit
1bb2302654
39
Cargo.toml
Normal file
39
Cargo.toml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
[package]
|
||||||
|
name = "billiards"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
egui = "0.28"
|
||||||
|
eframe = { version = "0.28", default-features = false, features = ["default_fonts", "glow"] }
|
||||||
|
log = "0.4"
|
||||||
|
serde = "1.0.208"
|
||||||
|
serde_derive = "1.0.208"
|
||||||
|
serde_json = "1.0.125"
|
||||||
|
|
||||||
|
# You only need serde if you want app persistence:
|
||||||
|
# serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
env_logger = "0.10"
|
||||||
|
rusqlite = "0.32.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
|
||||||
|
version = "0.3.4"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
rusqlite = "0.32.1"
|
||||||
|
serde_json = "1.0.125"
|
1
Trunk.toml
Normal file
1
Trunk.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
[build]
|
48
build.rs
Normal file
48
build.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
let dest_path = Path::new(&out_dir).join("builtin_tables.rs");
|
||||||
|
|
||||||
|
let configs = rusqlite::Connection::open("tables.db")
|
||||||
|
.unwrap()
|
||||||
|
.prepare("SELECT * FROM tables")
|
||||||
|
.unwrap()
|
||||||
|
.query_map([], |row| Ok((
|
||||||
|
row.get::<_, String>("name")?,
|
||||||
|
row.get::<_, String>("outline")?,
|
||||||
|
row.get::<_, f64>("pos_x")?,
|
||||||
|
row.get::<_, f64>("pos_y")?,
|
||||||
|
row.get::<_, f64>("vec_x")?,
|
||||||
|
row.get::<_, f64>("vec_y")?,
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
for (name, outline, pos_x, pos_y, vec_x, vec_y) in &configs {
|
||||||
|
let outline_parsed: Vec<(f64, f64)> =
|
||||||
|
serde_json::from_str(outline)
|
||||||
|
.expect("outline field should be a valid [(f64, f64)]");
|
||||||
|
|
||||||
|
let outline_str: String = outline_parsed
|
||||||
|
.into_iter()
|
||||||
|
.map(|x|format!("({:.15},{:.15}),",x.0,x.1))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
lines.push(format!(
|
||||||
|
"BilliardConfiguration {{ name: {:?}.into(), outline: vec![{}], initial_pos: ({:.15}, {:.15}), initial_vec: ({:.15}, {:.15}) }},\n",
|
||||||
|
name, &outline_str, *pos_x, *pos_y, *vec_x, *vec_y
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
&dest_path,
|
||||||
|
"vec![".to_string() + &lines.iter().map(|x|x.as_str()).collect::<String>() + "]"
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
println!("cargo::rerun-if-changed=build.rs");
|
||||||
|
}
|
202
src/app.rs
Normal file
202
src/app.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use egui::{ComboBox, Slider, Context, CentralPanel, Layout, Align};
|
||||||
|
use crate::table::Table;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use rusqlite::Connection;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
pub struct BilliardConfiguration {
|
||||||
|
pub name: String,
|
||||||
|
pub outline: Vec<(f64, f64)>,
|
||||||
|
pub initial_pos: (f64, f64),
|
||||||
|
pub initial_vec: (f64, f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static BUILTIN_TABLES: LazyLock<Vec<BilliardConfiguration>> =
|
||||||
|
LazyLock::new(|| include!(concat!(env!("OUT_DIR"), "/builtin_tables.rs")));
|
||||||
|
|
||||||
|
pub struct BilliardsApp {
|
||||||
|
table: Table,
|
||||||
|
speed: f64,
|
||||||
|
stopped: bool,
|
||||||
|
last_time: f64,
|
||||||
|
table_index: usize,
|
||||||
|
requested_table_index: usize,
|
||||||
|
table_names: Vec<String>,
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
db: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BilliardsApp {
|
||||||
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
let mut result = Self {
|
||||||
|
table: Table::new(
|
||||||
|
&BUILTIN_TABLES[0].outline,
|
||||||
|
BUILTIN_TABLES[0].initial_pos.0,
|
||||||
|
BUILTIN_TABLES[0].initial_pos.1,
|
||||||
|
BUILTIN_TABLES[0].initial_vec.0,
|
||||||
|
BUILTIN_TABLES[0].initial_vec.1,
|
||||||
|
0.0
|
||||||
|
),
|
||||||
|
speed: 1.0,
|
||||||
|
stopped: false,
|
||||||
|
last_time: get_elapsed_time(),
|
||||||
|
table_index: 0,
|
||||||
|
requested_table_index: 0,
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
db: Connection::open("tables.db").unwrap(),
|
||||||
|
|
||||||
|
table_names: BUILTIN_TABLES.iter().map(|x|x.name.clone()).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = result.update_table_names();
|
||||||
|
result.load_table_by_name(&result.table_names[0].clone());
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn update_table_names(&mut self) -> rusqlite::Result<()> {
|
||||||
|
self.table_names = self.db
|
||||||
|
.prepare("SELECT name FROM tables")?
|
||||||
|
.query_map([], |row| row.get(0))?
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn update_table_names(&mut self) -> Result<(), std::convert::Infallible> {
|
||||||
|
// do nothing
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn load_table_by_name(&mut self, name: &str) {
|
||||||
|
let get_from_db = || -> rusqlite::Result<_> { Ok(
|
||||||
|
self.db
|
||||||
|
.prepare("SELECT * FROM tables WHERE name = :name")?
|
||||||
|
.query_row(&[(":name", name)], |row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, String>("outline")?,
|
||||||
|
row.get::<_, f64>("pos_x")?,
|
||||||
|
row.get::<_, f64>("pos_y")?,
|
||||||
|
row.get::<_, f64>("vec_x")?,
|
||||||
|
row.get::<_, f64>("vec_y")?
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
)};
|
||||||
|
|
||||||
|
let Ok((outline, pos_x, pos_y, vec_x, vec_y)) = get_from_db()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(outline_decoded) = serde_json::from_str::<Vec<(f64, f64)>>(&outline)
|
||||||
|
else {
|
||||||
|
println!("Invalid outline!");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.table.set_outline_and_initial(
|
||||||
|
&outline_decoded, pos_x, pos_y, vec_x, vec_y, 0.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn load_table_by_name(&mut self, name: &str) {
|
||||||
|
for conf in &*BUILTIN_TABLES {
|
||||||
|
if conf.name == name {
|
||||||
|
self.table.set_outline_and_initial(
|
||||||
|
&conf.outline,
|
||||||
|
conf.initial_pos.0,
|
||||||
|
conf.initial_pos.1,
|
||||||
|
conf.initial_vec.0,
|
||||||
|
conf.initial_vec.1,
|
||||||
|
0.0
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
static WEB: bool = cfg!(target_arch = "wasm32");
|
||||||
|
|
||||||
|
impl eframe::App for BilliardsApp {
|
||||||
|
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||||
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
|
|
||||||
|
|
||||||
|
ui.heading("Billiards simulator");
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ComboBox::from_id_source("outline")
|
||||||
|
.selected_text(&self.table_names[self.table_index])
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (i, n) in self.table_names.iter().enumerate() {
|
||||||
|
ui.selectable_value(&mut self.requested_table_index, i, n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let startstop = if self.stopped { "Start" } else { "Stop" };
|
||||||
|
if ui.button(startstop).clicked() {
|
||||||
|
self.stopped = !self.stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Reset").clicked() {
|
||||||
|
self.table.set_time(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add(Slider::new(&mut self.speed, 0.0 ..= 10.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Combobox selection changed!
|
||||||
|
if self.requested_table_index != self.table_index {
|
||||||
|
self.table_index = self.requested_table_index;
|
||||||
|
let table_name = self.table_names[self.table_index].to_string();
|
||||||
|
|
||||||
|
self.load_table_by_name(&table_name);
|
||||||
|
|
||||||
|
// while we're accessing the database anyway, also update the list of names
|
||||||
|
let _ = self.update_table_names();
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = get_elapsed_time();
|
||||||
|
let frametime = elapsed - self.last_time;
|
||||||
|
self.last_time = elapsed;
|
||||||
|
if !self.stopped {
|
||||||
|
self.table.add_time(frametime * self.speed);
|
||||||
|
}
|
||||||
|
ui.add_sized(ui.available_size(), &mut self.table);
|
||||||
|
if !self.stopped {
|
||||||
|
ui.ctx().request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.with_layout(Layout::bottom_up(Align::LEFT), |ui| {
|
||||||
|
egui::warn_if_debug_build(ui);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return the time elapsed since an arbitrary moment, in seconds
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn get_elapsed_time() -> f64 {
|
||||||
|
static TIME_ORIGIN: LazyLock<Instant> = LazyLock::new(Instant::now);
|
||||||
|
|
||||||
|
Instant::now().duration_since(*TIME_ORIGIN).as_nanos() as f64 * 1e-9
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn get_elapsed_time() -> f64 {
|
||||||
|
web_sys::window().unwrap().performance().unwrap().now() * 1e-3
|
||||||
|
}
|
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod table;
|
68
src/main.rs
Normal file
68
src/main.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
use billiards::app::BilliardsApp;
|
||||||
|
|
||||||
|
// native
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn main() -> eframe::Result {
|
||||||
|
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||||
|
|
||||||
|
let native_options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_inner_size([400.0, 300.0])
|
||||||
|
.with_min_inner_size([300.0, 220.0])
|
||||||
|
.with_icon(
|
||||||
|
// NOTE: Adding an icon is optional
|
||||||
|
eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..])
|
||||||
|
.expect("Failed to load icon"),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native(
|
||||||
|
"Billiards simulator",
|
||||||
|
native_options,
|
||||||
|
Box::new(|cc| Ok(Box::new(BilliardsApp::new(cc)))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasm
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn main() {
|
||||||
|
// Redirect `log` message to `console.log` and friends:
|
||||||
|
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
|
||||||
|
|
||||||
|
let web_options = eframe::WebOptions {
|
||||||
|
follow_system_theme: false,
|
||||||
|
default_theme: eframe::Theme::Dark,
|
||||||
|
.. eframe::WebOptions::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
wasm_bindgen_futures::spawn_local(async {
|
||||||
|
let start_result = eframe::WebRunner::new()
|
||||||
|
.start(
|
||||||
|
"the_canvas_id",
|
||||||
|
web_options,
|
||||||
|
Box::new(|cc| Ok(Box::new(BilliardsApp::new(cc)))),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Remove the loading text and spinner:
|
||||||
|
let loading_text = web_sys::window()
|
||||||
|
.and_then(|w| w.document())
|
||||||
|
.and_then(|d| d.get_element_by_id("loading_text"));
|
||||||
|
if let Some(loading_text) = loading_text {
|
||||||
|
match start_result {
|
||||||
|
Ok(_) => {
|
||||||
|
loading_text.remove();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
loading_text.set_inner_html(
|
||||||
|
"<p> The app has crashed. See the developer console for details. </p>",
|
||||||
|
);
|
||||||
|
panic!("Failed to start eframe: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
227
src/table.rs
Normal file
227
src/table.rs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
use egui::{Color32, Response, Ui, Widget, Stroke, Rect, Pos2};
|
||||||
|
|
||||||
|
const EPSILON: f64 = 1e-10;
|
||||||
|
|
||||||
|
// a time, point and a vector, the fundamental state of the simulation
|
||||||
|
#[derive(Debug,Clone,Copy)]
|
||||||
|
struct State {
|
||||||
|
t: f64,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
dx: f64,
|
||||||
|
dy: f64,
|
||||||
|
corner: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Table {
|
||||||
|
bbox: Rect,
|
||||||
|
outline: Vec<(f64, f64)>,
|
||||||
|
path: Vec<State>,
|
||||||
|
time: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Table {
|
||||||
|
pub fn new(outline: &[(f64, f64)], x: f64, y: f64, dx: f64, dy: f64, time: f64) -> Table {
|
||||||
|
let mut result = Table {
|
||||||
|
bbox: Rect { min: Pos2 {x: 0.0, y: 0.0}, max: Pos2 {x: 0.0, y: 0.0}},
|
||||||
|
outline: outline.to_vec(),
|
||||||
|
path: vec![State { t: 0.0, x, y, dx, dy, corner: false }],
|
||||||
|
time: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.update_bbox();
|
||||||
|
result.set_initial(x, y, dx, dy, time);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_bbox(&mut self) {
|
||||||
|
let mut bbox = Rect {
|
||||||
|
min: Pos2 {x: f32::MAX, y: f32::MAX },
|
||||||
|
max: Pos2 {x: f32::MIN, y: f32::MIN },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (x,y) in &self.outline {
|
||||||
|
bbox.max.x = f32::max(bbox.max.x, *x as f32);
|
||||||
|
bbox.max.y = f32::max(bbox.max.y, *y as f32);
|
||||||
|
bbox.min.x = f32::min(bbox.min.x, *x as f32);
|
||||||
|
bbox.min.y = f32::min(bbox.min.y, *y as f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bbox = bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// find the intersection of the ray originating from `state` and the line segment
|
||||||
|
/// between `a` and `b`. Returns `None` if there is no intersection.
|
||||||
|
/// Intersections in "negative direction" are not counted.
|
||||||
|
fn intersect(state: State, a: (f64, f64), b: (f64, f64)) -> Option<State> {
|
||||||
|
let vnprod = state.dx * (a.1 - b.1) - state.dy * (a.0 - b.0);
|
||||||
|
let nnprod = (a.0 - b.0) * (a.0 - b.0) + (a.1 - b.1) * (a.1 - b.1);
|
||||||
|
let s = (state.dx * (state.y - b.1) - state.dy * (state.x - b.0)) / vnprod;
|
||||||
|
let dt = ((a.0 - b.0) * (state.y - b.1) - (a.1 - b.1) * (state.x - b.0)) / vnprod;
|
||||||
|
|
||||||
|
if s < -EPSILON || s > 1.0 + EPSILON || dt < EPSILON {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(State {
|
||||||
|
t: state.t + dt,
|
||||||
|
x: state.x + dt * state.dx,
|
||||||
|
y: state.y + dt * state.dy,
|
||||||
|
dx: state.dx - 2.0 * vnprod / nnprod * (a.1 - b.1),
|
||||||
|
dy: state.dy - 2.0 * vnprod / nnprod * (b.0 - a.0),
|
||||||
|
corner: s.abs() < EPSILON || (s-1.0).abs() < EPSILON,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn precompute_path(&mut self, time_limit: f64) {
|
||||||
|
let mut last_state = *self.path.last().unwrap();
|
||||||
|
|
||||||
|
while last_state.t < time_limit {
|
||||||
|
if last_state.corner {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the first intersection
|
||||||
|
let mut next_state = None;
|
||||||
|
let mut next_time = f64::MAX;
|
||||||
|
let outline_len = self.outline.len();
|
||||||
|
for i in 0..outline_len {
|
||||||
|
let int = Self::intersect(last_state, self.outline[i], self.outline[(i+1)%outline_len]);
|
||||||
|
if let Some(st) = int {
|
||||||
|
if st.t > last_state.t && st.t < next_time {
|
||||||
|
next_time = st.t;
|
||||||
|
next_state = Some(st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(next_state) = next_state {
|
||||||
|
self.path.push(next_state);
|
||||||
|
last_state = next_state;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let Some(next_state) = next_state else { panic!("Table has to be a closed polygon!"); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_time(&mut self, time: f64) {
|
||||||
|
self.time = time;
|
||||||
|
|
||||||
|
let endpoint = self.path.last().unwrap();
|
||||||
|
// extend 10 seconds in the future just for good measure
|
||||||
|
if !endpoint.corner && time + 10.0 > endpoint.t {
|
||||||
|
self.precompute_path(time + 10.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_time(&self) -> f64 {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_time(&mut self, time: f64) {
|
||||||
|
self.set_time(self.time + time);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_initial(&mut self, x: f64, y: f64, dx: f64, dy: f64, time: f64) {
|
||||||
|
self.path.truncate(0);
|
||||||
|
self.path.push(State { t: 0.0, x, y, dx, dy, corner: false });
|
||||||
|
self.set_time(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// resets time to 0, but keeps initial position and vector
|
||||||
|
pub fn set_outline(&mut self, outline: &[(f64, f64)]) {
|
||||||
|
self.outline = outline.to_vec();
|
||||||
|
self.path.truncate(1);
|
||||||
|
self.update_bbox();
|
||||||
|
self.set_time(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_outline_and_initial(&mut self, outline: &[(f64, f64)], x: f64, y: f64, dx: f64, dy: f64, time: f64) {
|
||||||
|
self.outline = outline.to_vec();
|
||||||
|
self.update_bbox();
|
||||||
|
self.path.truncate(0);
|
||||||
|
self.path.push(State { t: 0.0, x, y, dx, dy, corner: false });
|
||||||
|
self.set_time(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &mut Table {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let (response, painter) = ui.allocate_painter(ui.available_size(), egui::Sense::click());
|
||||||
|
|
||||||
|
let stroke_outline = Stroke::new(2.0, Color32::from_gray(255));
|
||||||
|
let stroke_path = Stroke::new(1.0, Color32::from_rgb(255, 0, 0));
|
||||||
|
|
||||||
|
let object_rect = self.bbox;
|
||||||
|
let screen_rect = response.rect;
|
||||||
|
|
||||||
|
let scale = 0.9 * f32::min(
|
||||||
|
(screen_rect.max.x - screen_rect.min.x) / (object_rect.max.x - object_rect.min.x),
|
||||||
|
(screen_rect.max.y - screen_rect.min.y) / (object_rect.max.y - object_rect.min.y));
|
||||||
|
let (offsetx, offsety) = (
|
||||||
|
0.5*(screen_rect.min.x + screen_rect.max.x
|
||||||
|
- scale*object_rect.min.x - scale*object_rect.max.x),
|
||||||
|
0.5*(screen_rect.min.y + screen_rect.max.y
|
||||||
|
- scale*object_rect.min.y - scale*object_rect.max.y));
|
||||||
|
|
||||||
|
let mut ball_pos: (f64, f64) = (0.0, 0.0);
|
||||||
|
|
||||||
|
// draw billiard path
|
||||||
|
let len = self.path.len();
|
||||||
|
for i in 1 .. len {
|
||||||
|
if self.path[i-1].t > self.time {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pointa = Pos2 {
|
||||||
|
x: self.path[i-1].x as f32 * scale + offsetx,
|
||||||
|
y: self.path[i-1].y as f32 * scale + offsety
|
||||||
|
};
|
||||||
|
|
||||||
|
ball_pos = if self.path[i].t < self.time {
|
||||||
|
(self.path[i].x, self.path[i].y)
|
||||||
|
} else {
|
||||||
|
let s = (self.time - self.path[i-1].t) /
|
||||||
|
(self.path[i].t - self.path[i-1].t);
|
||||||
|
|
||||||
|
(self.path[i-1].x + s * (self.path[i].x - self.path[i-1].x),
|
||||||
|
self.path[i-1].y + s * (self.path[i].y - self.path[i-1].y))
|
||||||
|
};
|
||||||
|
|
||||||
|
let pointb = Pos2 {
|
||||||
|
x: ball_pos.0 as f32 * scale + offsetx,
|
||||||
|
y: ball_pos.1 as f32 * scale + offsety
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
painter.line_segment([pointa, pointb], stroke_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw table
|
||||||
|
let len = self.outline.len();
|
||||||
|
for i in 0 .. len {
|
||||||
|
painter.line_segment([
|
||||||
|
Pos2 {
|
||||||
|
x: self.outline[i].0 as f32 * scale + offsetx,
|
||||||
|
y: self.outline[i].1 as f32 * scale + offsety
|
||||||
|
},
|
||||||
|
Pos2 {
|
||||||
|
x: self.outline[(i+1)%len].0 as f32 * scale + offsetx,
|
||||||
|
y: self.outline[(i+1)%len].1 as f32 * scale + offsety
|
||||||
|
}], stroke_outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw ball
|
||||||
|
let center = Pos2 {
|
||||||
|
x: ball_pos.0 as f32 * scale + offsetx,
|
||||||
|
y: ball_pos.1 as f32 * scale + offsety};
|
||||||
|
painter.circle_filled(center, 5.0, Color32::from_gray(255));
|
||||||
|
|
||||||
|
// println!("{:?} {:?}", &screen_rect, elapsed);
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
21
tables.sql
Normal file
21
tables.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
CREATE TABLE tables(name TEXT NOT NULL, pos_x REAL NOT NULL, pos_y REAL NOT NULL, vec_x REAL NOT NULL, vec_y REAL NOT NULL, outline TEXT);
|
||||||
|
INSERT INTO tables VALUES('Rectangle (periodic short)',-1,0,1.6,2.4,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 1.0], [-2.0, 1.0]]');
|
||||||
|
INSERT INTO tables VALUES('Rectangle (periodic long)',-1,0,7.04,8,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 1.0], [-2.0, 1.0]]');
|
||||||
|
INSERT INTO tables VALUES('Rectangle (dense)',-1,0,10,7.34819206,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 1.0], [-2.0, 1.0]]');
|
||||||
|
INSERT INTO tables VALUES('Rectangle (horizontal)',-1,0,2,0,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 1.0], [-2.0, 1.0]]');
|
||||||
|
INSERT INTO tables VALUES('Triangle 6 (dense)',0,-0.5,5,5,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 1.309401076758]]');
|
||||||
|
INSERT INTO tables VALUES('Triangle 6 (periodic)',0,-0.5,1,4.33012701892,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 1.309401076758]]');
|
||||||
|
INSERT INTO tables VALUES('Triangle 6 (orthogonal)',0.3,-0.3,0,-1.000000,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 1.309401076758]]');
|
||||||
|
INSERT INTO tables VALUES('Triangle 8 (periodic)',0.4,-0.7,-0.5,2.41421356237,'[[-2.0, -1.0], [2.0, -1.0], [2.0, 0.656854249492]]');
|
||||||
|
INSERT INTO tables VALUES('Dumbbell',-1.5,-0.1,1,2.12345567899,'[[-2.0, -1.0], [-1.0, -1.0], [-1.0, -0.05], [1.0, -0.05], [1.0, -1.0], [2.0, -1.0], [2.0, 1.0], [1.0, 1.0], [1.0, 0.05], [-1.0, 0.05], [-1.0, 1.0], [-2.0, 1.0]]');
|
||||||
|
INSERT INTO tables VALUES('L (dense)',0.2,0.5,0.5,-0.5,'[[1.0, 1.0], [1.0, 0.0], [2.276394569, 0.0], [2.276394569, -1.0], [-1.0, -1.0], [-1.0, 1.0]]');
|
||||||
|
INSERT INTO tables VALUES('L (periodic)',0.7,0.5,0.5,-0.5,'[[1.0, 1.0], [1.0, 0.0], [2.276394569, 0.0], [2.276394569, -1.0], [-1.0, -1.0], [-1.0, 1.0]]');
|
||||||
|
INSERT INTO tables VALUES('Nonconvex',0.4,0,0,-1.5,'[[0.0, -1.0], [-0.5, 1.8356409098088555], [0.5, 0.6438873172146455], [1.0, 1.0634371328032852], [1.5, 0.7133333636984291], [0.5502838083459437, -1.0]]');
|
||||||
|
INSERT INTO tables VALUES('Nonconvex 2',0.3,0,0,-1.5,'[[0.0, -1.0], [-0.5, 1.8356409098088555], [0.5, 0.6438873172146455], [1.0, 1.0634371328032852], [1.5, 0.7133333636984291], [0.5502838083459437, -1.0]]');
|
||||||
|
INSERT INTO tables VALUES('Orthogonal',1,0,0,-1,'[[-2.0, -1.0], [2.0, -1.0], [0.33512798812026023, 1.8836429126751244]]');
|
||||||
|
INSERT INTO tables VALUES('Hourglass',-0.2,-0.1,2,1,'[[-1.2, 1.0], [1.2, 1.0], [0.2, 0.0], [1.2, -1.0], [-1.2, -1.0], [-0.2, 0.0]]');
|
||||||
|
INSERT INTO tables VALUES('Star',0.2,0.5,0.5,-0.5,'[[0.0, 1.5], [0.5, 0.5], [1.5, 0.0], [0.5, -0.5], [0.0, -1.5], [-0.5, -0.5], [-1.5, 0.0], [-0.5, 0.5]]');
|
||||||
|
INSERT INTO tables VALUES('Star (short)',0.5,0,0.5,-0.8660254037844386,'[[0.0, 1.3660254037844386], [0.5, 0.5], [1.3660254037844386, 0.0], [0.5, -0.5], [0.0, -1.3660254037844386], [-0.5, -0.5], [-1.3660254037844386, 0.0], [-0.5, 0.5]]');
|
||||||
|
INSERT INTO tables VALUES('Tooth',-0.7,0.3,-1,-1,'[[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [0.0, 0.0], [-1.0, 1.0]]');
|
||||||
|
COMMIT;
|
Loading…
Reference in New Issue
Block a user