|
|
@ -1,76 +1,18 @@
|
|
|
|
use osu_file_parser::{
|
|
|
|
use osu_file_parser::{
|
|
|
|
osu_file::{
|
|
|
|
hitobjects::{HitObject, HitObjectParams::*, SlideParams, CurveType},
|
|
|
|
hitobjects::{
|
|
|
|
osu_file::hitobjects,
|
|
|
|
CurveType::*,
|
|
|
|
Decimal as OsuDecimal,
|
|
|
|
HitObjectParams::*
|
|
|
|
OsuFile,
|
|
|
|
}
|
|
|
|
Position
|
|
|
|
},
|
|
|
|
|
|
|
|
OsuFile
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
|
|
|
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
|
|
|
|
|
|
|
use kurbo::{BezPath, PathEl, Point, QuadSpline};
|
|
|
|
|
|
|
|
|
|
|
|
mod types;
|
|
|
|
mod types;
|
|
|
|
use types::*;
|
|
|
|
use types::*;
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
mod config;
|
|
|
|
The input file.
|
|
|
|
use config as cfg;
|
|
|
|
*/
|
|
|
|
|
|
|
|
const OSU: &str = include_str!("../osu/test2/test2.osu");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Machine's maximum acceleration in mm/s².
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const MAX_ACCEL: f64 = 110_000.0;
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
The default acceleration to use for max_v calculation.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const DEFAULT_ACCEL: f64 = MAX_ACCEL / 2.0;
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
The acceleration to use for groups of hitobjects. This should be higher as
|
|
|
|
|
|
|
|
groups of hitobjects usually have to be traveled with "linear" velocity
|
|
|
|
|
|
|
|
across all the hitobjects.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const GROUP_ACCEL: f64 = MAX_ACCEL;
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
How much to increase the acceleration if the current acceleration doesn't
|
|
|
|
|
|
|
|
produce a result.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const ACCEL_RETRY_STEP: f64 = 5_000.0;
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Since hitobjects' times are rounded to the nearest millisecond, linear
|
|
|
|
|
|
|
|
velocity calculations aren't very precise, leading to hitobjects with linear
|
|
|
|
|
|
|
|
velocities perceived to be identical not counted for grouping. To compensate
|
|
|
|
|
|
|
|
for this, a threshold is set, and any difference between linear velocities
|
|
|
|
|
|
|
|
under this threshold are counted.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const GROUP_VEL_LENIENCY: f64 = 0.2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Size of the bounding box for the beatmap in storage. Do not change.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const BOUNDING_BOX_SIZE: (f64, f64) = (512.0, 384.0); // lazer default ig
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Physical limits of the machine
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const PHYSICAL_MAX: (f64, f64) = (105.0, 110.0);
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Active area
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const AREA_ORIGIN: (f64, f64) = (10.0, 10.0);
|
|
|
|
|
|
|
|
const AREA_MAX: (f64, f64) = (95.0, 95.0);
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Rotation in degrees anteclockwise
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const ROTATION: f64 = 90.0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const AREA_SIZE: (f64, f64) = (
|
|
|
|
|
|
|
|
AREA_MAX.0 - AREA_ORIGIN.0,
|
|
|
|
|
|
|
|
AREA_MAX.1 - AREA_ORIGIN.1
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
const BOX_TO_AREA_OFFSET: (f64, f64) = (
|
|
|
|
|
|
|
|
AREA_SIZE.0 / 2.0 - BOUNDING_BOX_SIZE.0 / 2.0,
|
|
|
|
|
|
|
|
AREA_SIZE.1 / 2.0 - BOUNDING_BOX_SIZE.1 / 2.0
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
/*
|
|
|
|
Calculates the maximum velocity (mm/ms) given the the start and end
|
|
|
|
Calculates the maximum velocity (mm/ms) given the the start and end
|
|
|
@ -142,8 +84,8 @@ fn solve_feedrate_and_accel(
|
|
|
|
loop {
|
|
|
|
loop {
|
|
|
|
max_v = max_velocity(p1, p2, accel, ms);
|
|
|
|
max_v = max_velocity(p1, p2, accel, ms);
|
|
|
|
if !max_v.is_nan() { break }
|
|
|
|
if !max_v.is_nan() { break }
|
|
|
|
accel += ACCEL_RETRY_STEP;
|
|
|
|
accel += cfg::ACCEL_RETRY_STEP;
|
|
|
|
if accel >= MAX_ACCEL {
|
|
|
|
if accel >= cfg::MAX_ACCEL {
|
|
|
|
panic!("couldn't calculate max_v for dist {} within {}ms", p1 >> p2, ms);
|
|
|
|
panic!("couldn't calculate max_v for dist {} within {}ms", p1 >> p2, ms);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -163,141 +105,366 @@ fn solve_feedrate_and_accel(
|
|
|
|
(feedrate, accel)
|
|
|
|
(feedrate, accel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn generate_gcode_move(
|
|
|
|
|
|
|
|
destination: PositionF64,
|
|
|
|
|
|
|
|
accel: f64,
|
|
|
|
|
|
|
|
last_accel: f64,
|
|
|
|
|
|
|
|
feedrate: f64
|
|
|
|
|
|
|
|
) -> String {
|
|
|
|
|
|
|
|
let mut output = String::new();
|
|
|
|
|
|
|
|
if accel != last_accel {
|
|
|
|
|
|
|
|
output = format!("M204 S{}\n", accel);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
output = format!(
|
|
|
|
|
|
|
|
"{}G0 F{:.4}\nG0 X{} Y{}",
|
|
|
|
|
|
|
|
output,
|
|
|
|
|
|
|
|
feedrate,
|
|
|
|
|
|
|
|
destination.x, destination.y
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
println!("{}", output);
|
|
|
|
|
|
|
|
output
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn osu_position_to_kurbo_point(pos: &Position) -> Point {
|
|
|
|
|
|
|
|
Point {
|
|
|
|
|
|
|
|
x: pos.x.get().clone().expect_left("why").to_f64().expect("why"),
|
|
|
|
|
|
|
|
y: pos.y.get().clone().expect_left("why").to_f64().expect("why")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
/* Steps
|
|
|
|
/*
|
|
|
|
1. Read beatmap and hitcircles
|
|
|
|
# Calculating moves and optimizing
|
|
|
|
a. convert curves to lines
|
|
|
|
|
|
|
|
b. apply scaling for every point
|
|
|
|
The way this system works is by iterating through the hitobjects and
|
|
|
|
c. apply rotation for every point
|
|
|
|
"accumulating" state, then creating *moves* from said state. On each
|
|
|
|
2. Convert to gcode paths
|
|
|
|
iteration, we don't "move to the next point", instead, we decide whether
|
|
|
|
a. add preliminary gcode
|
|
|
|
or not to generate gcode to move to the last point. This also means that
|
|
|
|
b. calculate F for a given movement
|
|
|
|
every move starts at a point in the past. Here is a pseudocode
|
|
|
|
c. write movements to file
|
|
|
|
representation:
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
start of the move = the last bounding box corner
|
|
|
|
let src = OSU.parse::<OsuFile>()?;
|
|
|
|
last point = the last bounding box corner
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if it's the first point:
|
|
|
|
|
|
|
|
tell the machine to pause
|
|
|
|
|
|
|
|
otherwise:
|
|
|
|
|
|
|
|
if this point is in the same location as the last point:
|
|
|
|
|
|
|
|
enable stack mode
|
|
|
|
|
|
|
|
otherwise:
|
|
|
|
|
|
|
|
if stack mode is on:
|
|
|
|
|
|
|
|
generate gcode to move to the first point in the stack
|
|
|
|
|
|
|
|
generate gcode to dwell for the duration of the stack
|
|
|
|
|
|
|
|
disable stack mode
|
|
|
|
|
|
|
|
otherwise, if the hitobject qualifies for grouping:
|
|
|
|
|
|
|
|
increment the group size
|
|
|
|
|
|
|
|
otherwise:
|
|
|
|
|
|
|
|
generate gcode to move to the last point
|
|
|
|
|
|
|
|
store the last point as the start of the move
|
|
|
|
|
|
|
|
store the current point as the last point
|
|
|
|
|
|
|
|
increment total time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Keep in mind that a line of gcode only tells the pen to move to another
|
|
|
|
|
|
|
|
position, and the starting location is based on its current position,
|
|
|
|
|
|
|
|
which was the destination of the last move.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let src = cfg::OSU.parse::<OsuFile>()?;
|
|
|
|
let binding = src.hitobjects.expect("Beatmap has no hit objects!");
|
|
|
|
let binding = src.hitobjects.expect("Beatmap has no hit objects!");
|
|
|
|
let mut hitobjects = binding.0.iter();
|
|
|
|
|
|
|
|
let scale_fac = find_bbox_scale_factor(
|
|
|
|
let scale_fac = find_bbox_scale_factor(
|
|
|
|
BOUNDING_BOX_SIZE,
|
|
|
|
cfg::BOUNDING_BOX_SIZE,
|
|
|
|
AREA_SIZE,
|
|
|
|
cfg::AREA_SIZE,
|
|
|
|
ROTATION
|
|
|
|
cfg::ROTATION
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
println!("G90\nG0 Z0");
|
|
|
|
println!("G90\nG0 Z0");
|
|
|
|
let corners = [
|
|
|
|
let corners = [
|
|
|
|
AREA_ORIGIN,
|
|
|
|
cfg::AREA_ORIGIN,
|
|
|
|
(AREA_ORIGIN.0, AREA_MAX.1),
|
|
|
|
(cfg::AREA_ORIGIN.0, cfg::AREA_MAX.1),
|
|
|
|
(AREA_MAX.0, AREA_MAX.1),
|
|
|
|
(cfg::AREA_MAX.0, cfg::AREA_MAX.1),
|
|
|
|
(AREA_MAX.0, AREA_ORIGIN.1)
|
|
|
|
(cfg::AREA_MAX.0, cfg::AREA_ORIGIN.1)
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for corner in corners {
|
|
|
|
for corner in corners {
|
|
|
|
println!("G0 X{} Y{} F7500", corner.0, corner.1);
|
|
|
|
println!("G0 X{} Y{} F7500", corner.0, corner.1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut total_time: f64 = 0.0;
|
|
|
|
let final_corner = PositionF64::from_tuple(corners[corners.len() - 1]);
|
|
|
|
|
|
|
|
|
|
|
|
let mut p1 = PositionF64::new(0.0, 0.0);
|
|
|
|
// An extra element is needed at the end of the hitobjects vec
|
|
|
|
let mut p1_ms: Decimal = 0.into();
|
|
|
|
let temp_binding = binding.0.clone();
|
|
|
|
let mut last_accel: f64 = 0.0;
|
|
|
|
let mut last_hitobject = temp_binding.last().unwrap().clone();
|
|
|
|
|
|
|
|
last_hitobject.position = Position {
|
|
|
|
|
|
|
|
x: OsuDecimal::new(Decimal::from_f64_retain(final_corner.x).unwrap()),
|
|
|
|
|
|
|
|
y: OsuDecimal::new(Decimal::from_f64_retain(final_corner.y).unwrap()),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
let mut hitobjects = binding.0;
|
|
|
|
These are used for merging notes that share the same angle and linear
|
|
|
|
hitobjects.push(last_hitobject.clone());
|
|
|
|
velocity into one move. Linear velocity is plainly (dist / ms_delta).
|
|
|
|
let hitobjects = hitobjects.iter();
|
|
|
|
|
|
|
|
|
|
|
|
move_p1 is the starting point of a g-code move.
|
|
|
|
let mut s = IteratorState {
|
|
|
|
*/
|
|
|
|
total_time: 0,
|
|
|
|
let mut move_p1: PositionF64 = p1;
|
|
|
|
|
|
|
|
let mut move_p1_ms: Decimal = 0.into();
|
|
|
|
|
|
|
|
let mut group_linear_vel = 0.0;
|
|
|
|
|
|
|
|
let mut group_slope = 0.0;
|
|
|
|
|
|
|
|
let mut group_size = 0;
|
|
|
|
|
|
|
|
let mut dwell_state = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (count, obj) in hitobjects.enumerate() {
|
|
|
|
|
|
|
|
let p2_ms = obj.time.get().clone().unwrap_left();
|
|
|
|
|
|
|
|
let p1_to_p2_ms = p2_ms
|
|
|
|
|
|
|
|
.checked_sub(p1_ms)
|
|
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
|
|
.to_f64()
|
|
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let p2 = PositionF64
|
|
|
|
last_point: final_corner,
|
|
|
|
::from_position(obj.position.clone())
|
|
|
|
last_point_ms: 0,
|
|
|
|
.unwrap()
|
|
|
|
last_accel: 0.0,
|
|
|
|
.convert_to_machine_space(
|
|
|
|
|
|
|
|
scale_fac,
|
|
|
|
|
|
|
|
BOUNDING_BOX_SIZE,
|
|
|
|
|
|
|
|
AREA_ORIGIN,
|
|
|
|
|
|
|
|
AREA_SIZE,
|
|
|
|
|
|
|
|
ROTATION
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eprintln!("point {}", count);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if count == 0 {
|
|
|
|
|
|
|
|
println!("PAUSE");
|
|
|
|
|
|
|
|
} else if p1 == p2 && !dwell_state {
|
|
|
|
|
|
|
|
dwell_state = true;
|
|
|
|
|
|
|
|
} else if p1 != p2 {
|
|
|
|
|
|
|
|
let move_p1_to_p1_ms = p1_ms
|
|
|
|
|
|
|
|
.checked_sub(move_p1_ms)
|
|
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
|
|
.to_f64()
|
|
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let linear_vel = (p1 >> p2) / p1_to_p2_ms;
|
|
|
|
|
|
|
|
let slope = p1 / p2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
println!("; linear_vel {:.4}, slope {:.4}", linear_vel, slope);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if
|
|
|
|
|
|
|
|
linear_vel - group_linear_vel < GROUP_VEL_LENIENCY
|
|
|
|
|
|
|
|
&& slope == group_slope
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
println!("; !! merging point");
|
|
|
|
|
|
|
|
group_size += 1;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
let (feedrate, accel) = solve_feedrate_and_accel(
|
|
|
|
|
|
|
|
move_p1,
|
|
|
|
|
|
|
|
p1,
|
|
|
|
|
|
|
|
if group_size > 0 {
|
|
|
|
|
|
|
|
GROUP_ACCEL
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
DEFAULT_ACCEL
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
move_p1_to_p1_ms
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if accel != last_accel {
|
|
|
|
move_start: final_corner,
|
|
|
|
println!("M204 S{}", accel);
|
|
|
|
move_start_ms: 0,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
println!("G0 F{:.4}\nG0 X{} Y{}", feedrate, p1.x, p1.y);
|
|
|
|
group_linear_vel: 0.0,
|
|
|
|
|
|
|
|
group_slope: 0.0,
|
|
|
|
|
|
|
|
group_size: 0,
|
|
|
|
|
|
|
|
|
|
|
|
last_accel = accel;
|
|
|
|
stack: false,
|
|
|
|
group_linear_vel = linear_vel;
|
|
|
|
stack_start_ms: 0,
|
|
|
|
group_slope = slope;
|
|
|
|
|
|
|
|
group_size = 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if dwell_state {
|
|
|
|
osu_file: src
|
|
|
|
println!("G4 P{}", move_p1_to_p1_ms);
|
|
|
|
};
|
|
|
|
dwell_state = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
for (idx, obj) in hitobjects.enumerate() {
|
|
|
|
|
|
|
|
iteration(idx, obj, scale_fac, &mut s);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eprintln!("total time should be {}ms", s.total_time);
|
|
|
|
|
|
|
|
|
|
|
|
move_p1_ms = p1_ms;
|
|
|
|
Ok(())
|
|
|
|
move_p1 = p1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn iteration(
|
|
|
|
|
|
|
|
idx: usize,
|
|
|
|
|
|
|
|
obj: &HitObject,
|
|
|
|
|
|
|
|
scale_fac: f64,
|
|
|
|
|
|
|
|
s: &mut IteratorState
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
let current_ms = obj.time.get().clone().unwrap_left().to_u32().unwrap();
|
|
|
|
|
|
|
|
let ms_since_last_point = current_ms - s.last_point_ms;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let current_point = PositionF64
|
|
|
|
|
|
|
|
::from_position(obj.position.clone())
|
|
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
|
|
.convert_to_machine_space(
|
|
|
|
|
|
|
|
scale_fac,
|
|
|
|
|
|
|
|
cfg::BOUNDING_BOX_SIZE,
|
|
|
|
|
|
|
|
cfg::AREA_ORIGIN,
|
|
|
|
|
|
|
|
cfg::AREA_SIZE,
|
|
|
|
|
|
|
|
cfg::ROTATION
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eprintln!("point {}", idx);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if idx == 0 {
|
|
|
|
|
|
|
|
// Pause right before the first move
|
|
|
|
|
|
|
|
println!("PAUSE");
|
|
|
|
|
|
|
|
} else if let HitCircle = obj.obj_params {
|
|
|
|
|
|
|
|
handle_hitcircle(current_ms, ms_since_last_point, current_point, s);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if s.stack {
|
|
|
|
|
|
|
|
end_stack(s);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
end_move(s);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
p1 = p2;
|
|
|
|
match &obj.obj_params {
|
|
|
|
p1_ms = p2_ms;
|
|
|
|
Slider(params) => {
|
|
|
|
total_time += p1_to_p2_ms;
|
|
|
|
handle_slider(obj.position.clone(), ¶ms, s);
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
eprintln!("total time should be {}ms", total_time);
|
|
|
|
s.total_time += ms_since_last_point;
|
|
|
|
|
|
|
|
s.last_point = current_point;
|
|
|
|
|
|
|
|
s.last_point_ms = current_ms;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
fn handle_hitcircle(
|
|
|
|
|
|
|
|
current_ms: u32,
|
|
|
|
|
|
|
|
ms_since_last_point: u32,
|
|
|
|
|
|
|
|
current_point: PositionF64,
|
|
|
|
|
|
|
|
s: &mut IteratorState
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
if s.last_point == current_point && !s.stack {
|
|
|
|
|
|
|
|
// Begin stacking if the last point is the same as the current one
|
|
|
|
|
|
|
|
s.stack = true;
|
|
|
|
|
|
|
|
s.stack_start_ms = s.last_point_ms;
|
|
|
|
|
|
|
|
} else if s.last_point != current_point {
|
|
|
|
|
|
|
|
let linear_vel = (s.last_point >> current_point) / ms_since_last_point as f64;
|
|
|
|
|
|
|
|
let slope = s.last_point / current_point;
|
|
|
|
|
|
|
|
let can_group = s.group_linear_vel < cfg::GROUP_VEL_LENIENCY
|
|
|
|
|
|
|
|
&& slope == s.group_slope;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if s.stack {
|
|
|
|
|
|
|
|
end_stack(s);
|
|
|
|
|
|
|
|
} else if can_group {
|
|
|
|
|
|
|
|
println!("; !! adding point to group");
|
|
|
|
|
|
|
|
s.group_size += 1;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
end_move(s);
|
|
|
|
|
|
|
|
s.group_linear_vel = linear_vel;
|
|
|
|
|
|
|
|
s.group_slope = slope;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
This essentially works the same way as the base iteration. It creates
|
|
|
|
|
|
|
|
subpaths out of a set of control points by "accumulating" previous points
|
|
|
|
|
|
|
|
and deciding on whether or not to create a new subpath based on the next
|
|
|
|
|
|
|
|
point.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Sliders are saved as different types if they have multiple types of curves.
|
|
|
|
|
|
|
|
Essentially, a path becomes a bézier if it isn't completely linear. Perfect
|
|
|
|
|
|
|
|
circle subpaths get approximated as béziers, too.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fn handle_slider(
|
|
|
|
|
|
|
|
pos: Position,
|
|
|
|
|
|
|
|
params: &SlideParams,
|
|
|
|
|
|
|
|
s: &mut IteratorState
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
if params.curve_points.len() == 0 as usize { return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let points = ¶ms.curve_points
|
|
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
|
|
.map(|p| osu_position_to_kurbo_point(&p.0));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let ticks = match params.curve_type {
|
|
|
|
|
|
|
|
CurveType::Linear => { },
|
|
|
|
|
|
|
|
CurveType::PerfectCircle => { },
|
|
|
|
|
|
|
|
_ => { }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn calculate_linear_ticks(
|
|
|
|
|
|
|
|
pos: Position,
|
|
|
|
|
|
|
|
points: Vec<Point>,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
) -> Vec<PositionF64> {
|
|
|
|
|
|
|
|
if points.len() == 0 { panic!("wtf") }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut dist = 0.0;
|
|
|
|
|
|
|
|
let dist_between_ticks =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut ticks = Vec::<PositionF64>::new();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut dist_remainder = dist;
|
|
|
|
|
|
|
|
let mut point_iter = points.iter();
|
|
|
|
|
|
|
|
let mut p1 = PositionF64::from_point(*point_iter.next().unwrap());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'each_tick: while {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
|
|
|
let next_point = point_iter.next();
|
|
|
|
|
|
|
|
let p2 = PositionF64::from_point(*point_iter.next().unwrap());
|
|
|
|
|
|
|
|
let dist = p1 >> p2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if dist_remainder < dist {
|
|
|
|
|
|
|
|
let fac = dist_remainder / dist;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ticks.push(PositionF64 {
|
|
|
|
|
|
|
|
x: (p2.x - p1.x) * fac,
|
|
|
|
|
|
|
|
y: (p2.y - p1.y) * fac
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
} else if dist_remainder == dist {
|
|
|
|
|
|
|
|
ticks.push(p2);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
dist_remainder -= dist;
|
|
|
|
|
|
|
|
p1 = p2;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ticks
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn calculate_bezier_ticks(
|
|
|
|
|
|
|
|
pos: Position,
|
|
|
|
|
|
|
|
points: Vec<Point>,
|
|
|
|
|
|
|
|
slider_mul: f64,
|
|
|
|
|
|
|
|
slider_ticks: u8
|
|
|
|
|
|
|
|
) -> Vec<PositionF64> {
|
|
|
|
|
|
|
|
let last_point: Point;
|
|
|
|
|
|
|
|
let mut segment_vertices = Vec::<Point>::new();
|
|
|
|
|
|
|
|
segment_vertices.push(last_point);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut split_path = BezPath::from_vec(vec![PathEl::MoveTo(last_point)]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for point in points.iter() {
|
|
|
|
|
|
|
|
// Previous path ended, new subpath begins
|
|
|
|
|
|
|
|
if *point == last_point {
|
|
|
|
|
|
|
|
match segment_vertices.len() {
|
|
|
|
|
|
|
|
0 => { panic!("bruhhhhhh {:?}", pos) },
|
|
|
|
|
|
|
|
1 => { split_path.line_to(last_point) },
|
|
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
|
|
QuadSpline
|
|
|
|
|
|
|
|
::new(segment_vertices)
|
|
|
|
|
|
|
|
.to_quads()
|
|
|
|
|
|
|
|
.for_each(|quad| split_path.quad_to(quad.p1, quad.p2));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
segment_vertices = Vec::<Point>::new();
|
|
|
|
|
|
|
|
segment_vertices.push(last_point);
|
|
|
|
|
|
|
|
segment_vertices.push(*point);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
segment_vertices.push(*point);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
vec![PositionF64::new(10.0, 10.0)]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn end_stack(s: &mut IteratorState) {
|
|
|
|
|
|
|
|
// Generates gcode to move to the stack and wait there for the
|
|
|
|
|
|
|
|
// totkal duration of its hitobjects, and resets the stack state.
|
|
|
|
|
|
|
|
let (feedrate, accel) = solve_feedrate_and_accel(
|
|
|
|
|
|
|
|
s.move_start,
|
|
|
|
|
|
|
|
s.last_point,
|
|
|
|
|
|
|
|
cfg::DEFAULT_ACCEL,
|
|
|
|
|
|
|
|
(s.stack_start_ms - s.move_start_ms) as f64
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generate_gcode_move(s.last_point, accel, s.last_accel, feedrate);
|
|
|
|
|
|
|
|
println!("G4 P{}", s.last_point_ms - s.stack_start_ms);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s.last_accel = accel;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s.move_start_ms = s.last_point_ms;
|
|
|
|
|
|
|
|
s.move_start = s.last_point;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s.stack = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn end_move(s: &mut IteratorState) {
|
|
|
|
|
|
|
|
// Generates gcode for a move and starts a new one.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Use higher acceleration when moving across a group
|
|
|
|
|
|
|
|
let accel = if s.group_size > 0 {
|
|
|
|
|
|
|
|
cfg::GROUP_ACCEL
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
cfg::DEFAULT_ACCEL
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let (feedrate, accel) = solve_feedrate_and_accel(
|
|
|
|
|
|
|
|
s.move_start,
|
|
|
|
|
|
|
|
s.last_point,
|
|
|
|
|
|
|
|
accel,
|
|
|
|
|
|
|
|
(s.last_point_ms - s.move_start_ms) as f64
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generate_gcode_move(s.last_point, accel, s.last_accel, feedrate);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s.last_accel = accel;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s.move_start_ms = s.last_point_ms;
|
|
|
|
|
|
|
|
s.move_start = s.last_point;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s.group_size = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
/*
|
|
|
|