fix: Eingerasteter Textmarker behält Breite (Issue #32), runde Spitzen und ruhiger Strich beim Absetzen (Issue #33), zuverlässigeres Nachgleiten (Issue #28)
- Halten am Strichende rastet Formen in der Breite des gewählten Werkzeugs ein; ein eingerasteter Textmarker bleibt Textmarker und bildet kein Achsenkreuz - Ecken (Spitze einer „1“, Hochzahlen) werden rund verbunden statt stumpf abgeschnitten - Der Live-Strich nutzt dieselbe Tessellierung wie der fertige Strich, damit er beim Abheben nicht springt - Nachgleiten misst die Geschwindigkeit über die letzten 100 ms statt über einzelne Ereignisse; INK_NOTES_DEBUG_FLING gibt die Messung im Terminal aus Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
76968f9e58
commit
6a043802d7
5 files changed
+152
-27
No files matched your search
+5
-1
@@ -1324,9 +1324,13 @@ impl CanvasState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gleiche Tessellierung wie beim Absetzen (runde Kappen und Ecken), damit der Strich
|
||||||
|
// beim Abheben nicht sichtbar springt (Issue #33)
|
||||||
let color = self.current_ink_color;
|
let color = self.current_ink_color;
|
||||||
|
let points = spline::rdp_filter_samples(&self.live_samples, spline::RDP_TOLERANCE_UNITS);
|
||||||
|
let corners = spline::detect_corners(&points, spline::CORNER_ANGLE_THRESHOLD_DEG);
|
||||||
let raw_strip =
|
let raw_strip =
|
||||||
spline::generate_triangle_strip(&self.live_control_points, &self.spline_config);
|
spline::generate_finalized_triangle_strip(&points, &corners, &self.spline_config);
|
||||||
self.live_vertices = raw_strip
|
self.live_vertices = raw_strip
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| InkVertex::new(v.position[0], v.position[1], color))
|
.map(|v| InkVertex::new(v.position[0], v.position[1], color))
|
||||||
|
|||||||
+25
-7
@@ -326,9 +326,12 @@ fn commit_stroke_to_notebook(
|
|||||||
) {
|
) {
|
||||||
// Zweite Gerade über Kreuz mit dem vorigen Strich: beide werden zum Achsenkreuz; der
|
// Zweite Gerade über Kreuz mit dem vorigen Strich: beide werden zum Achsenkreuz; der
|
||||||
// vorige Strich wird ersetzt, ein Rückgängig stellt ihn wieder her
|
// vorige Strich wird ersetzt, ein Rückgängig stellt ihn wieder her
|
||||||
if let Some(shape) = &snapped_shape {
|
let stroke_width = ui_state.current_stroke_width_mm() * ink_notes::document::UNITS_PER_MM;
|
||||||
|
if let Some(shape) = snapped_shape
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| ui_state.active_tool != ActiveTool::Highlighter)
|
||||||
|
{
|
||||||
if let Some((previous_id, cross)) = axis_cross_with_previous(notebook, shape) {
|
if let Some((previous_id, cross)) = axis_cross_with_previous(notebook, shape) {
|
||||||
let stroke_width = ui_state.pen_stroke_width_mm * ink_notes::document::UNITS_PER_MM;
|
|
||||||
canvas.cancel_stroke();
|
canvas.cancel_stroke();
|
||||||
let stroke = geometry::shape_to_stroke(
|
let stroke = geometry::shape_to_stroke(
|
||||||
cross,
|
cross,
|
||||||
@@ -347,7 +350,6 @@ fn commit_stroke_to_notebook(
|
|||||||
|
|
||||||
let new_id = notebook.next_stroke_id();
|
let new_id = notebook.next_stroke_id();
|
||||||
let stroke_opt = if let Some(shape) = snapped_shape {
|
let stroke_opt = if let Some(shape) = snapped_shape {
|
||||||
let stroke_width = ui_state.pen_stroke_width_mm * ink_notes::document::UNITS_PER_MM;
|
|
||||||
let color = canvas.current_ink_color;
|
let color = canvas.current_ink_color;
|
||||||
canvas.cancel_stroke();
|
canvas.cancel_stroke();
|
||||||
Some(geometry::shape_to_stroke(
|
Some(geometry::shape_to_stroke(
|
||||||
@@ -1931,9 +1933,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
stroke_snapped_shape = Some(shape.clone());
|
stroke_snapped_shape = Some(shape.clone());
|
||||||
// Kreuzt die Gerade den vorigen Strich, zeigt die Vorschau schon das
|
// Kreuzt die Gerade den vorigen Strich, zeigt die Vorschau schon das
|
||||||
// Achsenkreuz (übernommen wird es beim Absetzen)
|
// Achsenkreuz (übernommen wird es beim Absetzen)
|
||||||
let shape = axis_cross_with_previous(¬ebook, &shape)
|
// Textmarker bilden kein Achsenkreuz
|
||||||
.map_or(shape, |(_, cross)| cross);
|
let shape = if ui_state.active_tool == ActiveTool::Highlighter {
|
||||||
let width_units = ui_state.pen_stroke_width_mm * ink_notes::document::UNITS_PER_MM;
|
shape
|
||||||
|
} else {
|
||||||
|
axis_cross_with_previous(¬ebook, &shape)
|
||||||
|
.map_or(shape, |(_, cross)| cross)
|
||||||
|
};
|
||||||
|
let width_units = ui_state.current_stroke_width_mm() * ink_notes::document::UNITS_PER_MM;
|
||||||
let (samples, vertices) = geometry::shape_to_mesh(&shape, width_units, canvas.current_ink_color);
|
let (samples, vertices) = geometry::shape_to_mesh(&shape, width_units, canvas.current_ink_color);
|
||||||
canvas.set_live_shape_mesh(samples, vertices);
|
canvas.set_live_shape_mesh(samples, vertices);
|
||||||
ui_state.set_export_message(t!("{shape} eingerastet!", shape = shape.name()));
|
ui_state.set_export_message(t!("{shape} eingerastet!", shape = shape.name()));
|
||||||
@@ -3019,7 +3026,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
pinch_prev_distance = None;
|
pinch_prev_distance = None;
|
||||||
pinch_prev_center = None;
|
pinch_prev_center = None;
|
||||||
if is_touch_panning && touch.phase == TouchPhase::Ended {
|
if is_touch_panning && touch.phase == TouchPhase::Ended {
|
||||||
fling.release(Instant::now());
|
// Letztes Stück bis zum Abheben noch mitnehmen
|
||||||
|
let (dx, dy) = (tx - touch_pan_last_pos.0, ty - touch_pan_last_pos.1);
|
||||||
|
canvas.pan(dx, dy);
|
||||||
|
fling.track(Vec2::new(dx, dy), Instant::now());
|
||||||
|
let velocity = fling.release(Instant::now());
|
||||||
|
if std::env::var_os("INK_NOTES_DEBUG_FLING").is_some() {
|
||||||
|
eprintln!(
|
||||||
|
"[Nachgleiten] {:.0} px/s beim Abheben, gleitet: {}",
|
||||||
|
velocity.length(),
|
||||||
|
fling.is_active()
|
||||||
|
);
|
||||||
|
}
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
is_touch_panning = false;
|
is_touch_panning = false;
|
||||||
|
|||||||
+64
-3
@@ -601,6 +601,7 @@ pub fn generate_finalized_triangle_strip(
|
|||||||
|
|
||||||
// Runde Kappen fein genug unterteilt, dass sie auch stark vergrößert rund wirken
|
// Runde Kappen fein genug unterteilt, dass sie auch stark vergrößert rund wirken
|
||||||
const CAP_STEPS: usize = 10;
|
const CAP_STEPS: usize = 10;
|
||||||
|
const CORNER_STEPS: usize = 24;
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
|
|
||||||
// 3. Runde Start-Kappe vor dem ersten Run
|
// 3. Runde Start-Kappe vor dem ersten Run
|
||||||
@@ -622,12 +623,22 @@ pub fn generate_finalized_triangle_strip(
|
|||||||
// 4. Runs und Ecken-Übergänge verbinden
|
// 4. Runs und Ecken-Übergänge verbinden
|
||||||
for (run_idx, run) in runs.iter().enumerate() {
|
for (run_idx, run) in runs.iter().enumerate() {
|
||||||
if run_idx > 0 {
|
if run_idx > 0 {
|
||||||
// Ecken-Verbindung: Nahtlose C0-Verbindung am Knick
|
// Runde Ecken-Verbindung: Ein voller Kreis um den Knick füllt die Außenseite, sonst
|
||||||
|
// enden beide Abschnitte stumpf und die Spitze (etwa einer „1“) wirkt abgeschnitten.
|
||||||
|
// Alle Dreiecke liegen im Kreis, auch die Übergänge zum vorigen und nächsten Run.
|
||||||
let s_corner = &run[0];
|
let s_corner = &run[0];
|
||||||
let p_c = s_corner.position;
|
let p_c = s_corner.position;
|
||||||
let pr_c = s_corner.pressure;
|
let pr_c = s_corner.pressure;
|
||||||
vertices.push(Vertex::new(p_c.x, p_c.y, pr_c, 0.0));
|
let r = s_corner.radius;
|
||||||
vertices.push(Vertex::new(p_c.x, p_c.y, pr_c, 0.0));
|
let prev_right = runs[run_idx - 1].last().map_or(s_corner.right, |s| s.right);
|
||||||
|
let d = prev_right - p_c;
|
||||||
|
let a0 = d.y.atan2(d.x);
|
||||||
|
for k in 0..=CORNER_STEPS {
|
||||||
|
let a = a0 + (k as f32 / CORNER_STEPS as f32) * std::f32::consts::TAU;
|
||||||
|
let rim = p_c + glam::Vec2::new(a.cos(), a.sin()) * r;
|
||||||
|
vertices.push(Vertex::new(p_c.x, p_c.y, pr_c, 0.0));
|
||||||
|
vertices.push(Vertex::new(rim.x, rim.y, pr_c, 1.0));
|
||||||
|
}
|
||||||
vertices.push(Vertex::new(s_corner.left.x, s_corner.left.y, pr_c, -1.0));
|
vertices.push(Vertex::new(s_corner.left.x, s_corner.left.y, pr_c, -1.0));
|
||||||
vertices.push(Vertex::new(s_corner.right.x, s_corner.right.y, pr_c, 1.0));
|
vertices.push(Vertex::new(s_corner.right.x, s_corner.right.y, pr_c, 1.0));
|
||||||
}
|
}
|
||||||
@@ -900,6 +911,56 @@ mod tests {
|
|||||||
assert!((corner_pt.position.y - 0.0).abs() < 1e-3);
|
assert!((corner_pt.position.y - 0.0).abs() < 1e-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Liegt `p` in einem der Dreiecke des Triangle-Strips?
|
||||||
|
fn strip_covers(vertices: &[InkVertex], p: glam::Vec2) -> bool {
|
||||||
|
let pos = |v: &InkVertex| glam::Vec2::new(v.position[0], v.position[1]);
|
||||||
|
let cross = |a: glam::Vec2, b: glam::Vec2, c: glam::Vec2| (b - a).perp_dot(c - a);
|
||||||
|
vertices.windows(3).any(|t| {
|
||||||
|
let (a, b, c) = (pos(&t[0]), pos(&t[1]), pos(&t[2]));
|
||||||
|
let (d1, d2, d3) = (cross(a, b, p), cross(b, c, p), cross(c, a, p));
|
||||||
|
let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
|
||||||
|
let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
|
||||||
|
!(neg && pos)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sharp_corner_tip_is_not_cut_off() {
|
||||||
|
// „1“: schräg hinauf bis (100, 0), dann senkrecht hinunter (Issue #33)
|
||||||
|
let mut raw = Vec::new();
|
||||||
|
for i in 0..=20 {
|
||||||
|
let u = i as f32 / 20.0;
|
||||||
|
raw.push(StylusSample {
|
||||||
|
position: glam::Vec2::new(60.0 + 40.0 * u, 80.0 - 80.0 * u),
|
||||||
|
pressure: 0.5,
|
||||||
|
timestamp: i as f64 * 0.01,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for i in 1..=30 {
|
||||||
|
raw.push(StylusSample {
|
||||||
|
position: glam::Vec2::new(100.0, i as f32 * 5.0),
|
||||||
|
pressure: 0.5,
|
||||||
|
timestamp: (20 + i) as f64 * 0.01,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let config = SplineConfig::from_width_units(10.0);
|
||||||
|
let stroke = finalize_stroke_with_config(1, &raw, [0.0; 4], &config);
|
||||||
|
assert_eq!(
|
||||||
|
detect_corners(&stroke.points, CORNER_ANGLE_THRESHOLD_DEG).len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
// Die Spitze reicht rund um den Knick so weit wie die Strichbreite
|
||||||
|
let r = calculate_radius(0.5, config.min_radius, config.max_radius) * 0.95;
|
||||||
|
for deg in [-150.0_f32, -120.0, -90.0, -60.0, -30.0, 0.0] {
|
||||||
|
let a = deg.to_radians();
|
||||||
|
let p = glam::Vec2::new(100.0, 0.0) + glam::Vec2::new(a.cos(), a.sin()) * r;
|
||||||
|
assert!(
|
||||||
|
strip_covers(&stroke.vertices, p),
|
||||||
|
"Spitze bei {deg}° abgeschnitten"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_small_loops_stay_round() {
|
fn test_small_loops_stay_round() {
|
||||||
// Kleines „o“ (Radius 0,8 mm) in 0,1 s mit 200 Hz, mit Vorschub wie im Wort
|
// Kleines „o“ (Radius 0,8 mm) in 0,1 s mit 200 Hz, mit Vorschub wie im Wort
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ impl ActiveTool {
|
|||||||
/// Werkzeug, das ein mit diesem Werkzeug gezeichneter Strich erhält.
|
/// Werkzeug, das ein mit diesem Werkzeug gezeichneter Strich erhält.
|
||||||
pub fn stroke_tool(&self, has_shape: bool) -> StrokeTool {
|
pub fn stroke_tool(&self, has_shape: bool) -> StrokeTool {
|
||||||
match self {
|
match self {
|
||||||
_ if has_shape => StrokeTool::Shape,
|
// Ein eingerasteter Textmarker bleibt Textmarker (durchscheinend, eigener Radierer)
|
||||||
ActiveTool::Highlighter => StrokeTool::Highlighter,
|
ActiveTool::Highlighter => StrokeTool::Highlighter,
|
||||||
|
_ if has_shape => StrokeTool::Shape,
|
||||||
_ => StrokeTool::Pen,
|
_ => StrokeTool::Pen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5806,6 +5807,11 @@ mod tests {
|
|||||||
// Eingerastete Form (Formwerkzeug oder Halten am Strichende)
|
// Eingerastete Form (Formwerkzeug oder Halten am Strichende)
|
||||||
assert_eq!(ActiveTool::Shape.stroke_tool(true), StrokeTool::Shape);
|
assert_eq!(ActiveTool::Shape.stroke_tool(true), StrokeTool::Shape);
|
||||||
assert_eq!(ActiveTool::Pen.stroke_tool(true), StrokeTool::Shape);
|
assert_eq!(ActiveTool::Pen.stroke_tool(true), StrokeTool::Shape);
|
||||||
|
// Eingerasteter Textmarker (Issue #32)
|
||||||
|
assert_eq!(
|
||||||
|
ActiveTool::Highlighter.stroke_tool(true),
|
||||||
|
StrokeTool::Highlighter
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+51
-15
@@ -308,9 +308,11 @@ impl ViewportState {
|
|||||||
/// Schwung beim Scrollen mit dem Finger (Issue #28): Nach dem Abheben gleitet der
|
/// Schwung beim Scrollen mit dem Finger (Issue #28): Nach dem Abheben gleitet der
|
||||||
/// Ausschnitt mit der zuletzt gewischten Geschwindigkeit weiter und bremst langsam ab.
|
/// Ausschnitt mit der zuletzt gewischten Geschwindigkeit weiter und bremst langsam ab.
|
||||||
/// Geschwindigkeiten in Bildschirmpixeln je Sekunde.
|
/// Geschwindigkeiten in Bildschirmpixeln je Sekunde.
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Fling {
|
pub struct Fling {
|
||||||
velocity: Vec2,
|
velocity: Vec2,
|
||||||
|
/// Fingerweg (aufsummiert) der letzten [`Self::WINDOW`] mit Zeitpunkt
|
||||||
|
trail: std::collections::VecDeque<(std::time::Instant, Vec2)>,
|
||||||
/// Zeit der letzten Fingerbewegung bzw. des letzten Gleitschritts
|
/// Zeit der letzten Fingerbewegung bzw. des letzten Gleitschritts
|
||||||
last: Option<std::time::Instant>,
|
last: Option<std::time::Instant>,
|
||||||
active: bool,
|
active: bool,
|
||||||
@@ -320,37 +322,53 @@ impl Fling {
|
|||||||
/// Abbremsen je Sekunde (Anteil der Geschwindigkeit, der nach 1 s bleibt: e^-FRICTION)
|
/// Abbremsen je Sekunde (Anteil der Geschwindigkeit, der nach 1 s bleibt: e^-FRICTION)
|
||||||
const FRICTION: f32 = 2.5;
|
const FRICTION: f32 = 2.5;
|
||||||
/// Langsamer wischen gleitet nicht nach
|
/// Langsamer wischen gleitet nicht nach
|
||||||
const MIN_START_SPEED: f32 = 400.0;
|
const MIN_START_SPEED: f32 = 250.0;
|
||||||
/// Darunter hört das Gleiten auf
|
/// Darunter hört das Gleiten auf
|
||||||
const MIN_SPEED: f32 = 40.0;
|
const MIN_SPEED: f32 = 40.0;
|
||||||
/// Liegt der Finger vor dem Abheben so lange still, gleitet nichts nach
|
/// Liegt der Finger vor dem Abheben so lange still, gleitet nichts nach
|
||||||
const MAX_REST: std::time::Duration = std::time::Duration::from_millis(80);
|
const MAX_REST: std::time::Duration = std::time::Duration::from_millis(120);
|
||||||
|
/// Die Geschwindigkeit ergibt sich aus dem Weg in dieser Zeit vor dem Abheben. Über
|
||||||
|
/// mehrere Ereignisse gemessen, weil Touchscreens kurz vor dem Abheben oft nur noch
|
||||||
|
/// winzige Bewegungen melden und Ereignisse gebündelt ankommen, wenn ein Bild länger
|
||||||
|
/// zum Zeichnen braucht.
|
||||||
|
const WINDOW: std::time::Duration = std::time::Duration::from_millis(100);
|
||||||
|
|
||||||
/// Finger hat sich um `delta` verschoben.
|
/// Finger hat sich um `delta` verschoben.
|
||||||
pub fn track(&mut self, delta: Vec2, now: std::time::Instant) {
|
pub fn track(&mut self, delta: Vec2, now: std::time::Instant) {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
let dt = self
|
let pos = self.trail.back().map_or(Vec2::ZERO, |&(_, p)| p) + delta;
|
||||||
.last
|
self.trail.push_back((now, pos));
|
||||||
.map_or(0.0, |last| now.duration_since(last).as_secs_f32());
|
// Einen Punkt vor dem Fenster behalten, damit es ganz abgedeckt ist
|
||||||
self.last = Some(now);
|
while self.trail.len() > 2 && now.duration_since(self.trail[1].0) > Self::WINDOW {
|
||||||
if dt <= 0.0 {
|
self.trail.pop_front();
|
||||||
return;
|
}
|
||||||
|
if delta != Vec2::ZERO {
|
||||||
|
self.last = Some(now);
|
||||||
}
|
}
|
||||||
// Geglättet: einzelne ungleichmäßige Touch-Ereignisse sollen nicht zählen
|
|
||||||
let sample = delta / dt.max(0.004);
|
|
||||||
self.velocity = self.velocity.lerp(sample, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finger abgehoben: gleitet weiter, wenn zuletzt schnell genug gewischt wurde.
|
/// Finger abgehoben: gleitet weiter, wenn zuletzt schnell genug gewischt wurde. Gibt die
|
||||||
pub fn release(&mut self, now: std::time::Instant) {
|
/// gemessene Geschwindigkeit zurück (für die Fehlersuche).
|
||||||
|
pub fn release(&mut self, now: std::time::Instant) -> Vec2 {
|
||||||
let resting = self
|
let resting = self
|
||||||
.last
|
.last
|
||||||
.is_none_or(|last| now.duration_since(last) > Self::MAX_REST);
|
.is_none_or(|last| now.duration_since(last) > Self::MAX_REST);
|
||||||
self.active = !resting && self.velocity.length() >= Self::MIN_START_SPEED;
|
self.velocity = match (self.trail.front(), self.trail.back()) {
|
||||||
|
(Some(&(t0, p0)), Some(&(_, p1))) => {
|
||||||
|
// Bis zum Abheben gemessen; gebündelte Ereignisse zählen mindestens 10 ms
|
||||||
|
let dt = now.duration_since(t0).as_secs_f32().max(0.01);
|
||||||
|
(p1 - p0) / dt
|
||||||
|
}
|
||||||
|
_ => Vec2::ZERO,
|
||||||
|
};
|
||||||
|
self.trail.clear();
|
||||||
|
let velocity = self.velocity;
|
||||||
|
self.active = !resting && velocity.length() >= Self::MIN_START_SPEED;
|
||||||
self.last = Some(now);
|
self.last = Some(now);
|
||||||
if !self.active {
|
if !self.active {
|
||||||
self.stop();
|
self.stop();
|
||||||
}
|
}
|
||||||
|
velocity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hält an (neue Berührung, Mausklick, Mausrad).
|
/// Hält an (neue Berührung, Mausklick, Mausrad).
|
||||||
@@ -432,6 +450,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
rested.release(ms(400));
|
rested.release(ms(400));
|
||||||
assert!(!rested.is_active());
|
assert!(!rested.is_active());
|
||||||
|
|
||||||
|
// Touchscreens melden kurz vor dem Abheben oft nur noch winzige Bewegungen
|
||||||
|
let mut lifted = Fling::default();
|
||||||
|
for i in 0..=8 {
|
||||||
|
lifted.track(Vec2::new(0.0, -30.0), ms(i * 10));
|
||||||
|
}
|
||||||
|
lifted.track(Vec2::new(0.0, -1.0), ms(90));
|
||||||
|
lifted.track(Vec2::new(0.0, 0.0), ms(100));
|
||||||
|
lifted.release(ms(104));
|
||||||
|
assert!(lifted.is_active());
|
||||||
|
|
||||||
|
// Gebündelt ankommende Ereignisse (ein Bild brauchte 50 ms)
|
||||||
|
let mut batched = Fling::default();
|
||||||
|
for t in [0, 50, 50, 50, 50, 100, 100, 100, 100] {
|
||||||
|
batched.track(Vec2::new(0.0, -20.0), ms(t));
|
||||||
|
}
|
||||||
|
batched.release(ms(100));
|
||||||
|
assert!(batched.is_active());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user