move spline stuff out of editor into libosu
This commit is contained in:
parent
00d640bdcf
commit
ff36531a62
9 changed files with 107 additions and 333 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1038,7 +1038,7 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libosu"
|
name = "libosu"
|
||||||
version = "0.0.12"
|
version = "0.0.12"
|
||||||
source = "git+https://github.com/iptq/libosu?rev=d75fe384e95156b3b40dd9e5ca7af539e48af8fa#d75fe384e95156b3b40dd9e5ca7af539e48af8fa"
|
source = "git+https://github.com/iptq/libosu?rev=f15cfd0c6331ba7ad76cc0acc0eb5c11cb145cb0#f15cfd0c6331ba7ad76cc0acc0eb5c11cb145cb0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
@ -1047,6 +1047,7 @@ dependencies = [
|
||||||
"num-derive 0.3.3",
|
"num-derive 0.3.3",
|
||||||
"num-rational 0.3.2",
|
"num-rational 0.3.2",
|
||||||
"num-traits 0.2.14",
|
"num-traits 0.2.14",
|
||||||
|
"ordered-float 2.0.1",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -20,4 +20,4 @@ ordered-float = "2.0.1"
|
||||||
|
|
||||||
[dependencies.libosu]
|
[dependencies.libosu]
|
||||||
git = "https://github.com/iptq/libosu"
|
git = "https://github.com/iptq/libosu"
|
||||||
rev = "d75fe384e95156b3b40dd9e5ca7af539e48af8fa"
|
rev = "f15cfd0c6331ba7ad76cc0acc0eb5c11cb145cb0"
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
nightly
|
|
68
src/beatmap.rs
Normal file
68
src/beatmap.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use libosu::{Beatmap, HitObjectKind, Point};
|
||||||
|
|
||||||
|
use crate::hit_object::HitObjectExt;
|
||||||
|
|
||||||
|
const STACK_DISTANCE: f64 = 3.0;
|
||||||
|
|
||||||
|
pub struct BeatmapExt {
|
||||||
|
pub inner: Beatmap,
|
||||||
|
pub hit_objects: Vec<HitObjectExt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeatmapExt {
|
||||||
|
pub fn new(inner: Beatmap) -> Self {
|
||||||
|
let hit_objects = inner
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(HitObjectExt::new)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
BeatmapExt { inner, hit_objects }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_stacking(&mut self, start_idx: usize, end_idx: usize) {
|
||||||
|
let mut extended_end_idx = end_idx;
|
||||||
|
|
||||||
|
if end_idx < self.hit_objects.len() - 1 {
|
||||||
|
// Extend the end index to include objects they are stacked on
|
||||||
|
for i in (start_idx..=end_idx).rev() {
|
||||||
|
let mut stack_base_idx = i;
|
||||||
|
|
||||||
|
for n in stack_base_idx + 1..self.hit_objects.len() {
|
||||||
|
let stack_base_obj = &self.hit_objects[stack_base_idx];
|
||||||
|
if let HitObjectKind::Spinner(_) = &stack_base_obj.inner.kind {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let object_n = &self.hit_objects[n];
|
||||||
|
if let HitObjectKind::Spinner(_) = &object_n.inner.kind {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end_time = self.inner.get_hitobject_end_time(&stack_base_obj.inner);
|
||||||
|
let stack_threshold =
|
||||||
|
self.inner.difficulty.approach_preempt() as f64 * self.inner.stack_leniency;
|
||||||
|
|
||||||
|
// We are no longer within stacking range of the next object.
|
||||||
|
if (object_n.inner.start_time.0 - end_time.0) as f64 > stack_threshold {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stack_base_pos: Point<f64> = stack_base_obj.inner.pos.to_float().unwrap();
|
||||||
|
let object_n_pos: Point<f64> = object_n.inner.pos.to_float().unwrap();
|
||||||
|
// if stack_base_pos.distance(object_n_pos) < STACK_DISTANCE
|
||||||
|
// || (stack_base_obj.inner.kind.is_slider()
|
||||||
|
// && self
|
||||||
|
// .inner
|
||||||
|
// .get_hitobject_end_pos(stack_base_obj)
|
||||||
|
// .distance(object_n_pos)
|
||||||
|
// < STACK_DISTANCE)
|
||||||
|
// {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut extended_start_idx = start_idx;
|
||||||
|
}
|
||||||
|
}
|
38
src/game.rs
38
src/game.rs
|
@ -6,18 +6,19 @@ use std::path::Path;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ggez::{
|
use ggez::{
|
||||||
event::{EventHandler, KeyCode, KeyMods},
|
event::{EventHandler, KeyCode, KeyMods},
|
||||||
nalgebra::Point2,
|
|
||||||
graphics::{
|
graphics::{
|
||||||
self, Color, DrawMode, DrawParam, FillOptions, FilterMode, Mesh, Rect, StrokeOptions, Text,
|
self, Color, DrawMode, DrawParam, FillOptions, FilterMode, Mesh, Rect, StrokeOptions, Text,
|
||||||
WHITE,
|
WHITE,
|
||||||
},
|
},
|
||||||
|
nalgebra::Point2,
|
||||||
Context, GameError, GameResult,
|
Context, GameError, GameResult,
|
||||||
};
|
};
|
||||||
use libosu::{Beatmap, HitObject, HitObjectKind, Point, SpinnerInfo};
|
use libosu::{Beatmap, HitObject, HitObjectKind, Point, SpinnerInfo, Spline};
|
||||||
|
|
||||||
use crate::audio::{AudioEngine, Sound};
|
use crate::audio::{AudioEngine, Sound};
|
||||||
|
use crate::beatmap::BeatmapExt;
|
||||||
use crate::skin::Skin;
|
use crate::skin::Skin;
|
||||||
use crate::slider_render::{render_slider, Spline};
|
use crate::slider_render::render_slider;
|
||||||
|
|
||||||
pub type SliderCache = HashMap<Vec<Point<i32>>, Spline>;
|
pub type SliderCache = HashMap<Vec<Point<i32>>, Spline>;
|
||||||
|
|
||||||
|
@ -25,8 +26,7 @@ pub struct Game {
|
||||||
is_playing: bool,
|
is_playing: bool,
|
||||||
audio_engine: AudioEngine,
|
audio_engine: AudioEngine,
|
||||||
song: Option<Sound>,
|
song: Option<Sound>,
|
||||||
beatmap: Beatmap,
|
beatmap: BeatmapExt,
|
||||||
hit_objects: Vec<HitObject>,
|
|
||||||
pub skin: Skin,
|
pub skin: Skin,
|
||||||
frame: usize,
|
frame: usize,
|
||||||
slider_cache: SliderCache,
|
slider_cache: SliderCache,
|
||||||
|
@ -35,15 +35,15 @@ pub struct Game {
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn new() -> Result<Game> {
|
pub fn new() -> Result<Game> {
|
||||||
let audio_engine = AudioEngine::new()?;
|
let audio_engine = AudioEngine::new()?;
|
||||||
let beatmap = Beatmap::default();
|
|
||||||
let hit_objects = Vec::new();
|
|
||||||
let skin = Skin::new();
|
let skin = Skin::new();
|
||||||
|
|
||||||
|
let beatmap = Beatmap::default();
|
||||||
|
let beatmap = BeatmapExt::new(beatmap);
|
||||||
|
|
||||||
Ok(Game {
|
Ok(Game {
|
||||||
is_playing: false,
|
is_playing: false,
|
||||||
audio_engine,
|
audio_engine,
|
||||||
beatmap,
|
beatmap,
|
||||||
hit_objects,
|
|
||||||
song: None,
|
song: None,
|
||||||
skin,
|
skin,
|
||||||
frame: 0,
|
frame: 0,
|
||||||
|
@ -57,11 +57,13 @@ impl Game {
|
||||||
let mut file = File::open(&path)?;
|
let mut file = File::open(&path)?;
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
file.read_to_string(&mut contents)?;
|
file.read_to_string(&mut contents)?;
|
||||||
self.beatmap = Beatmap::from_osz(&contents)?;
|
|
||||||
|
let beatmap = Beatmap::from_osz(&contents)?;
|
||||||
|
self.beatmap = BeatmapExt::new(beatmap);
|
||||||
|
|
||||||
let dir = path.parent().unwrap();
|
let dir = path.parent().unwrap();
|
||||||
|
|
||||||
let song = Sound::create(dir.join(&self.beatmap.audio_filename))?;
|
let song = Sound::create(dir.join(&self.beatmap.inner.audio_filename))?;
|
||||||
song.set_position(113.0)?;
|
song.set_position(113.0)?;
|
||||||
self.song = Some(song);
|
self.song = Some(song);
|
||||||
|
|
||||||
|
@ -105,7 +107,7 @@ impl Game {
|
||||||
end_time: f64,
|
end_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeline_span = 6.0 / self.beatmap.timeline_zoom;
|
let timeline_span = 6.0 / self.beatmap.inner.timeline_zoom;
|
||||||
let timeline_left = time - timeline_span / 2.0;
|
let timeline_left = time - timeline_span / 2.0;
|
||||||
let timeline_right = time + timeline_span / 2.0;
|
let timeline_right = time + timeline_span / 2.0;
|
||||||
println!("left {:.3} right {:.3}", timeline_left, timeline_right);
|
println!("left {:.3} right {:.3}", timeline_left, timeline_right);
|
||||||
|
@ -125,13 +127,13 @@ impl Game {
|
||||||
graphics::draw(ctx, ¤t_line, DrawParam::default())?;
|
graphics::draw(ctx, ¤t_line, DrawParam::default())?;
|
||||||
|
|
||||||
let mut playfield_hitobjects = Vec::new();
|
let mut playfield_hitobjects = Vec::new();
|
||||||
let preempt = (self.beatmap.difficulty.approach_preempt() as f64) / 1000.0;
|
let preempt = (self.beatmap.inner.difficulty.approach_preempt() as f64) / 1000.0;
|
||||||
let fade_in = (self.beatmap.difficulty.approach_fade_time() as f64) / 1000.0;
|
let fade_in = (self.beatmap.inner.difficulty.approach_fade_time() as f64) / 1000.0;
|
||||||
|
|
||||||
// TODO: tighten this loop even more by binary searching for the start of the timeline and
|
// TODO: tighten this loop even more by binary searching for the start of the timeline and
|
||||||
// playfield hitobjects rather than looping through the entire beatmap, better yet, just
|
// playfield hitobjects rather than looping through the entire beatmap, better yet, just
|
||||||
// keeping track of the old index will probably be much faster
|
// keeping track of the old index will probably be much faster
|
||||||
for ho in self.beatmap.hit_objects.iter().rev() {
|
for ho in self.beatmap.inner.hit_objects.iter().rev() {
|
||||||
let ho_time = (ho.start_time.0 as f64) / 1000.0;
|
let ho_time = (ho.start_time.0 as f64) / 1000.0;
|
||||||
|
|
||||||
// draw in timeline
|
// draw in timeline
|
||||||
|
@ -141,7 +143,7 @@ impl Game {
|
||||||
let timeline_y = TIMELINE_BOUNDS.y;
|
let timeline_y = TIMELINE_BOUNDS.y;
|
||||||
println!(
|
println!(
|
||||||
" - [{}] {:.3}-{:.3} : {:.3}%",
|
" - [{}] {:.3}-{:.3} : {:.3}%",
|
||||||
self.beatmap.timeline_zoom,
|
self.beatmap.inner.timeline_zoom,
|
||||||
timeline_left,
|
timeline_left,
|
||||||
timeline_right,
|
timeline_right,
|
||||||
timeline_percent * 100.0
|
timeline_percent * 100.0
|
||||||
|
@ -173,7 +175,7 @@ impl Game {
|
||||||
match ho.kind {
|
match ho.kind {
|
||||||
HitObjectKind::Circle => end_time = ho_time,
|
HitObjectKind::Circle => end_time = ho_time,
|
||||||
HitObjectKind::Slider(_) => {
|
HitObjectKind::Slider(_) => {
|
||||||
let duration = self.beatmap.get_slider_duration(ho).unwrap();
|
let duration = self.beatmap.inner.get_slider_duration(ho).unwrap();
|
||||||
end_time = ho_time + duration / 1000.0;
|
end_time = ho_time + duration / 1000.0;
|
||||||
}
|
}
|
||||||
HitObjectKind::Spinner(SpinnerInfo {
|
HitObjectKind::Spinner(SpinnerInfo {
|
||||||
|
@ -192,7 +194,7 @@ impl Game {
|
||||||
let cs_scale = PLAYFIELD_BOUNDS.w / 640.0;
|
let cs_scale = PLAYFIELD_BOUNDS.w / 640.0;
|
||||||
let osupx_scale_x = PLAYFIELD_BOUNDS.w / 512.0;
|
let osupx_scale_x = PLAYFIELD_BOUNDS.w / 512.0;
|
||||||
let osupx_scale_y = PLAYFIELD_BOUNDS.h / 384.0;
|
let osupx_scale_y = PLAYFIELD_BOUNDS.h / 384.0;
|
||||||
let cs_osupx = self.beatmap.difficulty.circle_size_osupx();
|
let cs_osupx = self.beatmap.inner.difficulty.circle_size_osupx();
|
||||||
let cs_real = cs_osupx * cs_scale;
|
let cs_real = cs_osupx * cs_scale;
|
||||||
|
|
||||||
for draw_info in playfield_hitobjects.iter() {
|
for draw_info in playfield_hitobjects.iter() {
|
||||||
|
@ -210,7 +212,7 @@ impl Game {
|
||||||
&mut self.slider_cache,
|
&mut self.slider_cache,
|
||||||
ctx,
|
ctx,
|
||||||
PLAYFIELD_BOUNDS,
|
PLAYFIELD_BOUNDS,
|
||||||
&self.beatmap,
|
&self.beatmap.inner,
|
||||||
ho,
|
ho,
|
||||||
color,
|
color,
|
||||||
)?;
|
)?;
|
||||||
|
|
12
src/hit_object.rs
Normal file
12
src/hit_object.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use libosu::HitObject;
|
||||||
|
|
||||||
|
pub struct HitObjectExt {
|
||||||
|
pub inner: HitObject,
|
||||||
|
pub stacking: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HitObjectExt {
|
||||||
|
pub fn new(inner: HitObject) -> Self {
|
||||||
|
HitObjectExt { inner, stacking: 0 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
#![feature(vec_into_raw_parts)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate anyhow;
|
extern crate anyhow;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
@ -7,8 +5,9 @@ extern crate log;
|
||||||
extern crate bass_sys as bass;
|
extern crate bass_sys as bass;
|
||||||
|
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod beatmap;
|
||||||
mod game;
|
mod game;
|
||||||
mod math;
|
mod hit_object;
|
||||||
mod skin;
|
mod skin;
|
||||||
mod slider_render;
|
mod slider_render;
|
||||||
|
|
||||||
|
|
29
src/math.rs
29
src/math.rs
|
@ -1,29 +0,0 @@
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use libosu::Point;
|
|
||||||
use num::Float;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Math<T>(PhantomData<T>);
|
|
||||||
|
|
||||||
impl<T: Float> Math<T> {
|
|
||||||
pub fn circumcircle(p1: Point<T>, p2: Point<T>, p3: Point<T>) -> (Point<T>, T) {
|
|
||||||
let (x1, y1) = (p1.0, p1.1);
|
|
||||||
let (x2, y2) = (p2.0, p2.1);
|
|
||||||
let (x3, y3) = (p3.0, p3.1);
|
|
||||||
|
|
||||||
let two = num::cast::<_, T>(2.0).unwrap();
|
|
||||||
let d = two.mul_add(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2), T::zero());
|
|
||||||
let ux = ((x1 * x1 + y1 * y1) * (y2 - y3)
|
|
||||||
+ (x2 * x2 + y2 * y2) * (y3 - y1)
|
|
||||||
+ (x3 * x3 + y3 * y3) * (y1 - y2))
|
|
||||||
/ d;
|
|
||||||
let uy = ((x1 * x1 + y1 * y1) * (x3 - x2)
|
|
||||||
+ (x2 * x2 + y2 * y2) * (x1 - x3)
|
|
||||||
+ (x3 * x3 + y3 * y3) * (x2 - x1))
|
|
||||||
/ d;
|
|
||||||
|
|
||||||
let center = Point(ux, uy);
|
|
||||||
(center, center.distance(p1))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ggez::{
|
use ggez::{
|
||||||
graphics::{
|
graphics::{
|
||||||
|
@ -8,11 +6,9 @@ use ggez::{
|
||||||
nalgebra::Point2,
|
nalgebra::Point2,
|
||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
use libosu::{Beatmap, HitObject, HitObjectKind, Point, SliderSplineKind};
|
use libosu::{Beatmap, HitObject, HitObjectKind, Spline};
|
||||||
use ordered_float::NotNan;
|
|
||||||
|
|
||||||
use crate::game::SliderCache;
|
use crate::game::SliderCache;
|
||||||
use crate::math::Math;
|
|
||||||
|
|
||||||
pub fn render_slider<'a>(
|
pub fn render_slider<'a>(
|
||||||
slider_cache: &'a mut SliderCache,
|
slider_cache: &'a mut SliderCache,
|
||||||
|
@ -33,7 +29,7 @@ pub fn render_slider<'a>(
|
||||||
slider_cache.get(&control_points).unwrap()
|
slider_cache.get(&control_points).unwrap()
|
||||||
} else {
|
} else {
|
||||||
let new_spline =
|
let new_spline =
|
||||||
Spline::from_control(&slider_info.kind, &control_points, slider_info.pixel_length);
|
Spline::from_control(slider_info.kind, &control_points, slider_info.pixel_length);
|
||||||
slider_cache.insert(control_points.clone(), new_spline);
|
slider_cache.insert(control_points.clone(), new_spline);
|
||||||
slider_cache.get(&control_points).unwrap()
|
slider_cache.get(&control_points).unwrap()
|
||||||
};
|
};
|
||||||
|
@ -99,277 +95,3 @@ pub fn render_slider<'a>(
|
||||||
|
|
||||||
Ok(spline)
|
Ok(spline)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Spline {
|
|
||||||
spline_points: Vec<P>,
|
|
||||||
cumulative_lengths: Vec<NotNan<f64>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Spline {
|
|
||||||
fn from_control(
|
|
||||||
kind: &SliderSplineKind,
|
|
||||||
control_points: &[Point<i32>],
|
|
||||||
pixel_length: f64,
|
|
||||||
) -> Self {
|
|
||||||
// no matter what, if there's 2 control points, it's linear
|
|
||||||
let mut kind = kind.clone();
|
|
||||||
if control_points.len() == 2 {
|
|
||||||
kind = SliderSplineKind::Linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
let points = control_points
|
|
||||||
.iter()
|
|
||||||
.map(|p| Point(p.0 as f64, p.1 as f64))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let spline_points = match kind {
|
|
||||||
SliderSplineKind::Linear => {
|
|
||||||
let start = points[0];
|
|
||||||
let unit = (points[1] - points[0]).norm();
|
|
||||||
let end = points[0] + unit * pixel_length;
|
|
||||||
vec![start, end]
|
|
||||||
}
|
|
||||||
SliderSplineKind::Perfect => {
|
|
||||||
let (p1, p2, p3) = (points[0], points[1], points[2]);
|
|
||||||
let (center, radius) = Math::circumcircle(p1, p2, p3);
|
|
||||||
|
|
||||||
// find the t-values of the start and end of the slider
|
|
||||||
let t0 = (center.1 - p1.1).atan2(p1.0 - center.0);
|
|
||||||
let mut mid = (center.1 - p2.1).atan2(p2.0 - center.0);
|
|
||||||
let mut t1 = (center.1 - p3.1).atan2(p3.0 - center.0);
|
|
||||||
|
|
||||||
// make sure t0 is less than t1
|
|
||||||
while mid < t0 {
|
|
||||||
mid += std::f64::consts::TAU;
|
|
||||||
}
|
|
||||||
while t1 < t0 {
|
|
||||||
t1 += std::f64::consts::TAU;
|
|
||||||
}
|
|
||||||
if mid > t1 {
|
|
||||||
t1 -= std::f64::consts::TAU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// circumference is 2 * pi * r, slider length over circumference is length/(2 * pi * r)
|
|
||||||
let direction_unit = (t1 - t0) / (t1 - t0).abs();
|
|
||||||
let new_t1 = t0 + direction_unit * (pixel_length / radius);
|
|
||||||
|
|
||||||
let mut t = t0;
|
|
||||||
let mut c = Vec::new();
|
|
||||||
loop {
|
|
||||||
if !((new_t1 >= t0 && t < new_t1) || (new_t1 < t0 && t > new_t1)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rel = Point(t.cos() * radius, -t.sin() * radius);
|
|
||||||
c.push(center + rel);
|
|
||||||
|
|
||||||
t += (new_t1 - t0) / pixel_length;
|
|
||||||
}
|
|
||||||
c
|
|
||||||
}
|
|
||||||
SliderSplineKind::Bezier => {
|
|
||||||
let mut idx = 0;
|
|
||||||
let mut whole = Vec::new();
|
|
||||||
let mut cumul_length = 0.0;
|
|
||||||
let mut last_circ: Option<P> = None;
|
|
||||||
let mut check_push = |whole: &mut Vec<P>, point: P| -> bool {
|
|
||||||
if cumul_length < pixel_length {
|
|
||||||
whole.push(point);
|
|
||||||
if let Some(circ) = last_circ {
|
|
||||||
cumul_length += circ.distance(point);
|
|
||||||
}
|
|
||||||
last_circ = Some(point);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// split the curve by red-anchors
|
|
||||||
for i in 1..points.len() {
|
|
||||||
if points[i].0 == points[i - 1].0 && points[i].1 == points[i - 1].1 {
|
|
||||||
let spline = calculate_bezier(&points[idx..i]);
|
|
||||||
|
|
||||||
// check if it's equal to the last thing that was added to whole
|
|
||||||
if let Some(last) = whole.last() {
|
|
||||||
if spline[0] != *last {
|
|
||||||
check_push(&mut whole, spline[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
check_push(&mut whole, spline[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add points, making sure no 2 are the same
|
|
||||||
for points in spline.windows(2) {
|
|
||||||
if points[0] != points[1] {
|
|
||||||
if !check_push(&mut whole, points[1]) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
idx = i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let spline = calculate_bezier(&points[idx..]);
|
|
||||||
if let Some(last) = whole.last() {
|
|
||||||
if spline[0] != *last {
|
|
||||||
check_push(&mut whole, spline[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
check_push(&mut whole, spline[0]);
|
|
||||||
}
|
|
||||||
for points in spline.windows(2) {
|
|
||||||
if points[0] != points[1] {
|
|
||||||
if !check_push(&mut whole, points[1]) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
whole
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cumulative_lengths = Vec::with_capacity(spline_points.len());
|
|
||||||
let mut curr = 0.0;
|
|
||||||
// using NotNan here because these need to be binary-searched over
|
|
||||||
// and f64 isn't Ord
|
|
||||||
cumulative_lengths.push(unsafe { NotNan::unchecked_new(curr) });
|
|
||||||
for points in spline_points.windows(2) {
|
|
||||||
let dist = points[0].distance(points[1]);
|
|
||||||
curr += dist;
|
|
||||||
cumulative_lengths.push(unsafe { NotNan::unchecked_new(curr) });
|
|
||||||
}
|
|
||||||
|
|
||||||
Spline {
|
|
||||||
spline_points,
|
|
||||||
cumulative_lengths,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn point_at_length(&self, length: f64) -> P {
|
|
||||||
let length_notnan = unsafe { NotNan::unchecked_new(length) };
|
|
||||||
match self.cumulative_lengths.binary_search(&length_notnan) {
|
|
||||||
Ok(idx) => self.spline_points[idx],
|
|
||||||
Err(idx) => {
|
|
||||||
let n = self.spline_points.len();
|
|
||||||
if idx == 0 && self.spline_points.len() > 2 {
|
|
||||||
return self.spline_points[0];
|
|
||||||
} else if idx == n {
|
|
||||||
return self.spline_points[n - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
let (len1, len2) = (
|
|
||||||
self.cumulative_lengths[idx - 1].into_inner(),
|
|
||||||
self.cumulative_lengths[idx].into_inner(),
|
|
||||||
);
|
|
||||||
let proportion = (length - len1) / (len2 - len1);
|
|
||||||
|
|
||||||
let (p1, p2) = (self.spline_points[idx - 1], self.spline_points[idx]);
|
|
||||||
// println!(
|
|
||||||
// "len={:.3} idx={} len1={:.3} len2={:.3} prop={:.3} p1={:.3} p2={:.3}",
|
|
||||||
// length, idx, len1, len2, proportion, p1, p2
|
|
||||||
// );
|
|
||||||
assert!(p1 != p2);
|
|
||||||
(p2 - p1) * proportion + p1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type P = Point<f64>;
|
|
||||||
type V<T> = (*mut T, usize, usize);
|
|
||||||
fn calculate_bezier(points: &[P]) -> Vec<P> {
|
|
||||||
let points = points.to_vec();
|
|
||||||
let mut output = Vec::new();
|
|
||||||
let n = points.len() - 1;
|
|
||||||
let last = points[n];
|
|
||||||
|
|
||||||
let mut to_flatten = VecDeque::new();
|
|
||||||
let mut free_buffers = VecDeque::new();
|
|
||||||
|
|
||||||
to_flatten.push_back(points.into_raw_parts());
|
|
||||||
let mut p = n;
|
|
||||||
let buf1 = vec![Point(0.0, 0.0); p + 1].into_raw_parts();
|
|
||||||
let buf2 = vec![Point(0.0, 0.0); p * 2 + 1].into_raw_parts();
|
|
||||||
|
|
||||||
let left_child = buf2;
|
|
||||||
while !to_flatten.is_empty() {
|
|
||||||
let parent = to_flatten.pop_front().unwrap();
|
|
||||||
let parent_slice = unsafe { std::slice::from_raw_parts_mut(parent.0, parent.1) };
|
|
||||||
|
|
||||||
if bezier_flat_enough(parent_slice) {
|
|
||||||
bezier_approximate(parent_slice, &mut output, buf1, buf2, p + 1);
|
|
||||||
free_buffers.push_front(parent);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let right_child = if free_buffers.is_empty() {
|
|
||||||
let buf = vec![Point(0.0, 0.0); p + 1];
|
|
||||||
buf.into_raw_parts()
|
|
||||||
} else {
|
|
||||||
free_buffers.pop_front().unwrap()
|
|
||||||
};
|
|
||||||
bezier_subdivide(parent_slice, left_child, right_child, buf1, p + 1);
|
|
||||||
|
|
||||||
let left_child = unsafe { std::slice::from_raw_parts(left_child.0, left_child.1) };
|
|
||||||
for i in 0..p + 1 {
|
|
||||||
parent_slice[i] = left_child[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
to_flatten.push_front(right_child);
|
|
||||||
to_flatten.push_front(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
output.push(last);
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOLERANCE: f64 = 0.25;
|
|
||||||
fn bezier_flat_enough(curve: &[P]) -> bool {
|
|
||||||
for i in 1..(curve.len() - 1) {
|
|
||||||
let p = curve[i - 1] - curve[i] * 2.0 + curve[i + 1];
|
|
||||||
if p.0 * p.0 + p.1 * p.1 > TOLERANCE * TOLERANCE / 4.0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bezier_approximate(curve: &[P], output: &mut Vec<P>, buf1: V<P>, buf2: V<P>, count: usize) {
|
|
||||||
let l = buf2;
|
|
||||||
let r = buf1;
|
|
||||||
bezier_subdivide(curve, l, r, buf1, count);
|
|
||||||
|
|
||||||
let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) };
|
|
||||||
let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) };
|
|
||||||
for i in 0..(count - 1) {
|
|
||||||
l[count + i] = r[i + 1];
|
|
||||||
}
|
|
||||||
output.push(curve[0]);
|
|
||||||
|
|
||||||
for i in 1..(count - 1) {
|
|
||||||
let idx = 2 * i;
|
|
||||||
let p = (l[idx - 1] + l[idx] * 2.0 + l[idx + 1]) * 0.25;
|
|
||||||
output.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bezier_subdivide(curve: &[P], l: V<P>, r: V<P>, subdiv: V<P>, count: usize) {
|
|
||||||
let midpoints = unsafe { std::slice::from_raw_parts_mut(subdiv.0, subdiv.1) };
|
|
||||||
for i in 0..count {
|
|
||||||
midpoints[i] = curve[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) };
|
|
||||||
let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) };
|
|
||||||
for i in 0..count {
|
|
||||||
l[i] = midpoints[0];
|
|
||||||
r[count - i - 1] = midpoints[count - i - 1];
|
|
||||||
for j in 0..(count - i - 1) {
|
|
||||||
midpoints[j] = (midpoints[j] + midpoints[j + 1]) * 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue