From 1bb2302654c5433f15cc6410d8dbb13ffcfa0e7b Mon Sep 17 00:00:00 2001 From: Florian Stecker Date: Thu, 22 Aug 2024 22:40:34 -0400 Subject: [PATCH] initial version --- Cargo.toml | 39 +++++++++ Trunk.toml | 1 + build.rs | 48 +++++++++++ src/app.rs | 202 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/main.rs | 68 +++++++++++++++ src/table.rs | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++ tables.db | Bin 0 -> 8192 bytes tables.sql | 21 +++++ 9 files changed, 608 insertions(+) create mode 100644 Cargo.toml create mode 100644 Trunk.toml create mode 100644 build.rs create mode 100644 src/app.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/table.rs create mode 100644 tables.db create mode 100644 tables.sql diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..65342ea --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/Trunk.toml b/Trunk.toml new file mode 100644 index 0000000..a7b19c9 --- /dev/null +++ b/Trunk.toml @@ -0,0 +1 @@ +[build] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..9a3f47d --- /dev/null +++ b/build.rs @@ -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::, _>>() + .unwrap(); + + let mut lines: Vec = 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::() + "]" + ).unwrap(); + + println!("cargo::rerun-if-changed=build.rs"); +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..5c083ed --- /dev/null +++ b/src/app.rs @@ -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> = + 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, + + #[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::>()?; + + 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::>(&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 = 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 +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c99343d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod table; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0ee9882 --- /dev/null +++ b/src/main.rs @@ -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( + "

The app has crashed. See the developer console for details.

", + ); + panic!("Failed to start eframe: {e:?}"); + } + } + } + }); +} diff --git a/src/table.rs b/src/table.rs new file mode 100644 index 0000000..0499706 --- /dev/null +++ b/src/table.rs @@ -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, + 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 { + 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 + } +} diff --git a/tables.db b/tables.db new file mode 100644 index 0000000000000000000000000000000000000000..48674ed13e0ee487309684541c2d8bf74ae012ef GIT binary patch literal 8192 zcmeHKO=ufO6yCMt&d9cG$Dss6O{NWPC2*A8o&BW+$!P^SsCHv3B}Ux@*T3aXA%}Qkj9R=+>_YqO9j7)w!I(g6A8P}wfW12xHY&Lvu zDo_76Uj)XaA4yj(@*Z`MOqPIBEf#rlJ|Tf-PIuv29XV zrC?AK2KQrBBeBlZu0(naj`%aG&D>0BK1C^kq-fl<+g)6MF9R~VTQs2JPY>w$A`T>e z_M-rkfSA__3}cc(GQv`h#4fWP+;SWZ@$qESkW;}tyhPHN>ArbA=(P5m^>(`-IoAK4 zJ2=5IsjZ+Sktgp;fxJh#Q|UWRV3f#VDbaJc=HROt2<7h5dMi8#_5;7(q_v_A*GfuI z3K$!zW;wR4YKlg(i>WWRjZIy1RL!)=s;-lYakB6-fDrVYwZPvA{CnPgq{S84|BDw+ z#B1UxW01gM!_*Z=acsr54P%&ela!HdVb#)9-PHLK36l|GVjWv5)@+4bhQOa zFtO>_)K9uZY#EAXV`2mAhC{YSjJaKFmLwwWd+}GEfal!vg&ESPw5-b&lsocTjbOL2 zlP|^x!f3xHBl86YbIJ%-L7CAX= z*S-A`upmiJ%0yDfNeJgRW*=H_lur+Vd@@q_H>ZFK$(J&P&x zHoTpXry@tbkd5QO59`hRfF9f9{KnlfToOa+{a_MYC7@s!l)E~ZE-mkV+IjHp@>y_i zPk^FKNEpVADMrn}-_H+9ujKyx@v{%}@GSUyGFT<}DZdkZ{=;EV{F@)ArAb~YXR-4y D^=)X* literal 0 HcmV?d00001 diff --git a/tables.sql b/tables.sql new file mode 100644 index 0000000..7521f24 --- /dev/null +++ b/tables.sql @@ -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;