Type the rest of the audio-controller boundary (no more bare-f64 wrapping)

Push Beats/Seconds through the remaining controller methods that took a bare
f64 and let the audio thread wrap it in a newtype, so the caller's domain is
now compiler-checked (the seam that hid the recording bug):

- seek -> Seconds
- set_trim_start/set_trim_end -> Seconds / Option<Seconds> (metatrack, always seconds)
- add_midi_note, add_loaded_midi_clip, update_midi_clip_notes -> Beats
- add_automation_point, remove_automation_point, automation_add_keyframe,
  automation_remove_keyframe -> Beats

Command enums stay raw f64 transport; only the public signatures + call sites
change. No behavior change — every caller already passed the right domain, this
just makes it enforced.

Two deliberate exceptions, documented in place:
- trim_clip stays f64: the TrimClip handler interprets it as Seconds for a
  sampled-audio clip but Beats for a MIDI clip, so no single newtype fits;
  callers pass the clip's own trim value, which matches its content domain.
- The piano-roll MIDI note model stays f64 internally (a beats-only subsystem
  with no seconds anywhere); it's converted to Beats at the update_midi_clip_notes
  boundary in UpdateMidiNotesAction, same as trim_start f64 -> Seconds at add_audio_clip.
This commit is contained in:
Skyler Lehmkuhl 2026-07-11 14:33:14 -04:00
parent b5766672ee
commit 64bf9bb431
8 changed files with 64 additions and 51 deletions

View File

@ -3348,8 +3348,8 @@ impl EngineController {
} }
/// Seek to a specific position in seconds /// Seek to a specific position in seconds
pub fn seek(&mut self, seconds: f64) { pub fn seek(&mut self, seconds: Seconds) {
let _ = self.command_tx.push(Command::Seek(seconds)); let _ = self.command_tx.push(Command::Seek(seconds.seconds_to_f64()));
} }
/// Set track volume (0.0 = silence, 1.0 = unity gain) /// Set track volume (0.0 = silence, 1.0 = unity gain)
@ -3386,6 +3386,10 @@ impl EngineController {
/// Trim a clip's internal boundaries (changes which portion of source content is used) /// Trim a clip's internal boundaries (changes which portion of source content is used)
/// This also resets external_duration to match internal duration (disables looping) /// This also resets external_duration to match internal duration (disables looping)
/// Trim a clip's internal content bounds. The units are content-domain and depend on the
/// track type: SECONDS for a sampled-audio clip, BEATS for a MIDI clip (see the TrimClip
/// handler). Left as raw f64 because a single newtype can't express both; callers pass the
/// clip's own `trim_start`/`trim_end`, which already match its content domain.
pub fn trim_clip(&mut self, track_id: TrackId, clip_id: ClipId, new_internal_start: f64, new_internal_end: f64) { pub fn trim_clip(&mut self, track_id: TrackId, clip_id: ClipId, new_internal_start: f64, new_internal_end: f64) {
let _ = self.command_tx.push(Command::TrimClip(track_id, clip_id, new_internal_start, new_internal_end)); let _ = self.command_tx.push(Command::TrimClip(track_id, clip_id, new_internal_start, new_internal_end));
} }
@ -3450,13 +3454,13 @@ impl EngineController {
} }
/// Set metatrack trim start in seconds /// Set metatrack trim start in seconds
pub fn set_trim_start(&mut self, track_id: TrackId, trim_start: f64) { pub fn set_trim_start(&mut self, track_id: TrackId, trim_start: Seconds) {
let _ = self.command_tx.push(Command::SetTrimStart(track_id, trim_start)); let _ = self.command_tx.push(Command::SetTrimStart(track_id, trim_start.seconds_to_f64()));
} }
/// Set metatrack trim end in seconds (None = no end trim) /// Set metatrack trim end in seconds (None = no end trim)
pub fn set_trim_end(&mut self, track_id: TrackId, trim_end: Option<f64>) { pub fn set_trim_end(&mut self, track_id: TrackId, trim_end: Option<Seconds>) {
let _ = self.command_tx.push(Command::SetTrimEnd(track_id, trim_end)); let _ = self.command_tx.push(Command::SetTrimEnd(track_id, trim_end.map(|s| s.seconds_to_f64())));
} }
/// Create a new audio track /// Create a new audio track
@ -3615,17 +3619,18 @@ impl EngineController {
} }
/// Add a MIDI note to a clip /// Add a MIDI note to a clip
pub fn add_midi_note(&mut self, track_id: TrackId, clip_id: MidiClipId, time_offset: f64, note: u8, velocity: u8, duration: f64) { pub fn add_midi_note(&mut self, track_id: TrackId, clip_id: MidiClipId, time_offset: Beats, note: u8, velocity: u8, duration: Beats) {
let _ = self.command_tx.push(Command::AddMidiNote(track_id, clip_id, time_offset, note, velocity, duration)); let _ = self.command_tx.push(Command::AddMidiNote(track_id, clip_id, time_offset.beats_to_f64(), note, velocity, duration.beats_to_f64()));
} }
/// Add a pre-loaded MIDI clip to a track at the given timeline position /// Add a pre-loaded MIDI clip to a track at the given timeline position (beats)
pub fn add_loaded_midi_clip(&mut self, track_id: TrackId, clip: MidiClip, start_time: f64) { pub fn add_loaded_midi_clip(&mut self, track_id: TrackId, clip: MidiClip, start_time: Beats) {
let _ = self.command_tx.push(Command::AddLoadedMidiClip(track_id, clip, start_time)); let _ = self.command_tx.push(Command::AddLoadedMidiClip(track_id, clip, start_time.beats_to_f64()));
} }
/// Update all notes in a MIDI clip /// Update all notes in a MIDI clip. Note tuples are (start [beats], note, velocity, duration [beats]).
pub fn update_midi_clip_notes(&mut self, track_id: TrackId, clip_id: MidiClipId, notes: Vec<(f64, u8, u8, f64)>) { pub fn update_midi_clip_notes(&mut self, track_id: TrackId, clip_id: MidiClipId, notes: Vec<(Beats, u8, u8, Beats)>) {
let notes = notes.into_iter().map(|(t, n, v, d)| (t.beats_to_f64(), n, v, d.beats_to_f64())).collect();
let _ = self.command_tx.push(Command::UpdateMidiClipNotes(track_id, clip_id, notes)); let _ = self.command_tx.push(Command::UpdateMidiClipNotes(track_id, clip_id, notes));
} }
@ -3661,25 +3666,25 @@ impl EngineController {
&mut self, &mut self,
track_id: TrackId, track_id: TrackId,
lane_id: crate::audio::AutomationLaneId, lane_id: crate::audio::AutomationLaneId,
time: f64, time: Beats,
value: f32, value: f32,
curve: crate::audio::CurveType, curve: crate::audio::CurveType,
) { ) {
let _ = self.command_tx.push(Command::AddAutomationPoint( let _ = self.command_tx.push(Command::AddAutomationPoint(
track_id, lane_id, time, value, curve, track_id, lane_id, time.beats_to_f64(), value, curve,
)); ));
} }
/// Remove an automation point at a specific time /// Remove an automation point at a specific time (beats); tolerance is a beats delta
pub fn remove_automation_point( pub fn remove_automation_point(
&mut self, &mut self,
track_id: TrackId, track_id: TrackId,
lane_id: crate::audio::AutomationLaneId, lane_id: crate::audio::AutomationLaneId,
time: f64, time: Beats,
tolerance: f64, tolerance: Beats,
) { ) {
let _ = self.command_tx.push(Command::RemoveAutomationPoint( let _ = self.command_tx.push(Command::RemoveAutomationPoint(
track_id, lane_id, time, tolerance, track_id, lane_id, time.beats_to_f64(), tolerance.beats_to_f64(),
)); ));
} }
@ -3715,16 +3720,16 @@ impl EngineController {
/// Add a keyframe to an AutomationInput node /// Add a keyframe to an AutomationInput node
pub fn automation_add_keyframe(&mut self, track_id: TrackId, node_id: u32, pub fn automation_add_keyframe(&mut self, track_id: TrackId, node_id: u32,
time: f64, value: f32, interpolation: String, time: Beats, value: f32, interpolation: String,
ease_out: (f32, f32), ease_in: (f32, f32)) { ease_out: (f32, f32), ease_in: (f32, f32)) {
let _ = self.command_tx.push(Command::AutomationAddKeyframe( let _ = self.command_tx.push(Command::AutomationAddKeyframe(
track_id, node_id, time, value, interpolation, ease_out, ease_in)); track_id, node_id, time.beats_to_f64(), value, interpolation, ease_out, ease_in));
} }
/// Remove a keyframe from an AutomationInput node /// Remove a keyframe from an AutomationInput node
pub fn automation_remove_keyframe(&mut self, track_id: TrackId, node_id: u32, time: f64) { pub fn automation_remove_keyframe(&mut self, track_id: TrackId, node_id: u32, time: Beats) {
let _ = self.command_tx.push(Command::AutomationRemoveKeyframe( let _ = self.command_tx.push(Command::AutomationRemoveKeyframe(
track_id, node_id, time)); track_id, node_id, time.beats_to_f64()));
} }
/// Set the display name of an AutomationInput node /// Set the display name of an AutomationInput node

View File

@ -790,7 +790,7 @@ fn execute_command(
return Err("Usage: seek <seconds>".to_string()); return Err("Usage: seek <seconds>".to_string());
} }
let pos: f64 = parts[1].parse().map_err(|_| "Invalid position")?; let pos: f64 = parts[1].parse().map_err(|_| "Invalid position")?;
controller.seek(pos); controller.seek(crate::Seconds(pos));
app.set_status(format!("Seeked to {:.2}s", pos)); app.set_status(format!("Seeked to {:.2}s", pos));
} }
"track" => { "track" => {
@ -882,7 +882,7 @@ fn execute_command(
app.next_clip_id += 1; app.next_clip_id += 1;
// Send to audio engine with the start_time (clip content is separate from timeline position) // Send to audio engine with the start_time (clip content is separate from timeline position)
controller.add_loaded_midi_clip(track_id, midi_clip, start_time); controller.add_loaded_midi_clip(track_id, midi_clip, crate::Beats(start_time));
app.set_status(format!("Loaded {} ({} events, {:.2}s) to track {} at {:.2}s", app.set_status(format!("Loaded {} ({} events, {:.2}s) to track {} at {:.2}s",
file_path, event_count, duration, track_id, start_time)); file_path, event_count, duration, track_id, start_time));

View File

@ -211,8 +211,8 @@ impl Action for MoveClipInstancesAction {
// Check if this clip has a metatrack // Check if this clip has a metatrack
if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(*new_start)); controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(*new_start));
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, daw_backend::Seconds(instance.trim_start));
controller.set_trim_end(metatrack_id, instance.trim_end); controller.set_trim_end(metatrack_id, instance.trim_end.map(daw_backend::Seconds));
} }
} }
} }
@ -295,8 +295,8 @@ impl Action for MoveClipInstancesAction {
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) { if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(*old_start)); controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(*old_start));
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, daw_backend::Seconds(instance.trim_start));
controller.set_trim_end(metatrack_id, instance.trim_end); controller.set_trim_end(metatrack_id, instance.trim_end.map(daw_backend::Seconds));
} }
} }
} }

View File

@ -387,8 +387,8 @@ impl Action for TrimClipInstancesAction {
if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
// Instance already has new values after execute() // Instance already has new values after execute()
controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(instance.timeline_start)); controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(instance.timeline_start));
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, daw_backend::Seconds(instance.trim_start));
controller.set_trim_end(metatrack_id, instance.trim_end); controller.set_trim_end(metatrack_id, instance.trim_end.map(daw_backend::Seconds));
} }
} }
} }
@ -477,8 +477,8 @@ impl Action for TrimClipInstancesAction {
if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
// Instance already has old values after rollback() // Instance already has old values after rollback()
controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(instance.timeline_start)); controller.set_offset(metatrack_id, document.tempo_map().beats_to_seconds(instance.timeline_start));
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, daw_backend::Seconds(instance.trim_start));
controller.set_trim_end(metatrack_id, instance.trim_end); controller.set_trim_end(metatrack_id, instance.trim_end.map(daw_backend::Seconds));
} }
} }
} }

View File

@ -1,7 +1,13 @@
use crate::action::Action; use crate::action::Action;
use crate::document::Document; use crate::document::Document;
use daw_backend::Beats;
use uuid::Uuid; use uuid::Uuid;
/// Convert editor-side beats-domain note tuples to the typed backend form.
fn notes_to_beats(notes: &[(f64, u8, u8, f64)]) -> Vec<(Beats, u8, u8, Beats)> {
notes.iter().map(|&(t, n, v, d)| (Beats(t), n, v, Beats(d))).collect()
}
/// Action to update MIDI notes in a clip (supports undo/redo) /// Action to update MIDI notes in a clip (supports undo/redo)
/// ///
/// Stores the before and after note states. MIDI note data lives in the backend, /// Stores the before and after note states. MIDI note data lives in the backend,
@ -49,7 +55,8 @@ impl Action for UpdateMidiNotesAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or_else(|| format!("Layer {} not mapped to backend track", self.layer_id))?; .ok_or_else(|| format!("Layer {} not mapped to backend track", self.layer_id))?;
controller.update_midi_clip_notes(*track_id, self.midi_clip_id, self.new_notes.clone()); // Note times/durations are beats (MIDI content domain); assert that at the typed boundary.
controller.update_midi_clip_notes(*track_id, self.midi_clip_id, notes_to_beats(&self.new_notes));
Ok(()) Ok(())
} }
@ -68,7 +75,7 @@ impl Action for UpdateMidiNotesAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or_else(|| format!("Layer {} not mapped to backend track", self.layer_id))?; .ok_or_else(|| format!("Layer {} not mapped to backend track", self.layer_id))?;
controller.update_midi_clip_notes(*track_id, self.midi_clip_id, self.old_notes.clone()); controller.update_midi_clip_notes(*track_id, self.midi_clip_id, notes_to_beats(&self.old_notes));
Ok(()) Ok(())
} }

View File

@ -2,6 +2,7 @@
//! project scrub. Wired to the audio controller exactly like `TimelinePane`'s header. //! project scrub. Wired to the audio controller exactly like `TimelinePane`'s header.
use eframe::egui; use eframe::egui;
use daw_backend::Seconds;
use super::{icons, Palette}; use super::{icons, Palette};
use crate::panes::SharedPaneState; use crate::panes::SharedPaneState;
@ -32,7 +33,7 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState,
if let Some(controller_arc) = shared.audio_controller { if let Some(controller_arc) = shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
if *shared.is_playing { if *shared.is_playing {
controller.seek(*shared.playback_time); controller.seek(Seconds(*shared.playback_time));
controller.play(); controller.play();
} else { } else {
controller.pause(); controller.pause();
@ -101,7 +102,7 @@ pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState,
*shared.playback_time = new_time; *shared.playback_time = new_time;
if let Some(controller_arc) = shared.audio_controller { if let Some(controller_arc) = shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(new_time); controller.seek(Seconds(new_time));
} }
} }
} }

View File

@ -651,7 +651,7 @@ impl PianoRollPane {
*shared.playback_time = nt; *shared.playback_time = nt;
if let Some(ctrl) = shared.audio_controller.as_ref() { if let Some(ctrl) = shared.audio_controller.as_ref() {
if let Ok(mut c) = ctrl.lock() { if let Ok(mut c) = ctrl.lock() {
c.seek(nt); c.seek(Seconds(nt));
} }
} }
} }
@ -1764,7 +1764,7 @@ impl PianoRollPane {
let seek_time = snap_to_value(time.max(0.0), self.snap_value, tempo_map); let seek_time = snap_to_value(time.max(0.0), self.snap_value, tempo_map);
*shared.playback_time = seek_time; *shared.playback_time = seek_time;
if let Some(ctrl) = shared.audio_controller.as_ref() { if let Some(ctrl) = shared.audio_controller.as_ref() {
if let Ok(mut c) = ctrl.lock() { c.seek(seek_time); } if let Ok(mut c) = ctrl.lock() { c.seek(Seconds(seek_time)); }
} }
} }
} }

View File

@ -1110,7 +1110,7 @@ impl TimelinePane {
if let Some(controller_arc) = shared.audio_controller { if let Some(controller_arc) = shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(seek_to); controller.seek(Seconds(seek_to));
controller.set_metronome_enabled(true); controller.set_metronome_enabled(true);
if !*shared.is_playing { if !*shared.is_playing {
controller.play(); controller.play();
@ -5006,7 +5006,7 @@ impl TimelinePane {
*playback_time = kf_time; *playback_time = kf_time;
if let Some(controller_arc) = audio_controller { if let Some(controller_arc) = audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(kf_time); controller.seek(Seconds(kf_time));
} }
} }
} }
@ -5034,7 +5034,7 @@ impl TimelinePane {
// Seek immediately so it works while playing // Seek immediately so it works while playing
if let Some(controller_arc) = audio_controller { if let Some(controller_arc) = audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(new_time); controller.seek(Seconds(new_time));
} }
} }
} }
@ -5046,7 +5046,7 @@ impl TimelinePane {
*playback_time = new_time; *playback_time = new_time;
if let Some(controller_arc) = audio_controller { if let Some(controller_arc) = audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(new_time); controller.seek(Seconds(new_time));
} }
} }
} }
@ -5206,7 +5206,7 @@ impl PaneRenderer for TimelinePane {
*shared.playback_time = 0.0; *shared.playback_time = 0.0;
if let Some(controller_arc) = shared.audio_controller { if let Some(controller_arc) = shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(0.0); controller.seek(Seconds(0.0));
} }
} }
@ -5215,7 +5215,7 @@ impl PaneRenderer for TimelinePane {
*shared.playback_time = (*shared.playback_time - 0.1).max(0.0); *shared.playback_time = (*shared.playback_time - 0.1).max(0.0);
if let Some(controller_arc) = shared.audio_controller { if let Some(controller_arc) = shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(*shared.playback_time); controller.seek(Seconds(*shared.playback_time));
} }
} }
@ -5251,7 +5251,7 @@ impl PaneRenderer for TimelinePane {
*shared.playback_time = (*shared.playback_time + 0.1).min(self.duration); *shared.playback_time = (*shared.playback_time + 0.1).min(self.duration);
if let Some(controller_arc) = shared.audio_controller { if let Some(controller_arc) = shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(*shared.playback_time); controller.seek(Seconds(*shared.playback_time));
} }
} }
@ -5260,7 +5260,7 @@ impl PaneRenderer for TimelinePane {
*shared.playback_time = self.duration; *shared.playback_time = self.duration;
if let Some(controller_arc) = shared.audio_controller { if let Some(controller_arc) = shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.seek(self.duration); controller.seek(Seconds(self.duration));
} }
} }
@ -5692,7 +5692,7 @@ impl PaneRenderer for TimelinePane {
self.automation_cache.remove(&layer_id); self.automation_cache.remove(&layer_id);
} else if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) { } else if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) {
// time is already in beats (all automation x-axes use beats) // time is already in beats (all automation x-axes use beats)
controller.automation_add_keyframe(track_id, node_id, time, value, "linear".to_string(), (0.0, 0.0), (0.0, 0.0)); controller.automation_add_keyframe(track_id, node_id, Beats(time), value, "linear".to_string(), (0.0, 0.0), (0.0, 0.0));
// Optimistic cache update (beats) // Optimistic cache update (beats)
if let Some(lanes) = self.automation_cache.get_mut(&layer_id) { if let Some(lanes) = self.automation_cache.get_mut(&layer_id) {
if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) { if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) {
@ -5740,8 +5740,8 @@ impl PaneRenderer for TimelinePane {
self.automation_cache.remove(&layer_id); self.automation_cache.remove(&layer_id);
} else if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) { } else if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) {
// old_time / new_time are already in beats // old_time / new_time are already in beats
controller.automation_remove_keyframe(track_id, node_id, old_time); controller.automation_remove_keyframe(track_id, node_id, Beats(old_time));
controller.automation_add_keyframe(track_id, node_id, new_time, new_value, interpolation.clone(), ease_out, ease_in); controller.automation_add_keyframe(track_id, node_id, Beats(new_time), new_value, interpolation.clone(), ease_out, ease_in);
// Optimistic cache update (beats) // Optimistic cache update (beats)
if let Some(lanes) = self.automation_cache.get_mut(&layer_id) { if let Some(lanes) = self.automation_cache.get_mut(&layer_id) {
if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) { if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) {
@ -5769,7 +5769,7 @@ impl PaneRenderer for TimelinePane {
} }
} else if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) { } else if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) {
// time is already in beats // time is already in beats
controller.automation_remove_keyframe(track_id, node_id, time); controller.automation_remove_keyframe(track_id, node_id, Beats(time));
// Optimistic cache update (beats) // Optimistic cache update (beats)
if let Some(lanes) = self.automation_cache.get_mut(&layer_id) { if let Some(lanes) = self.automation_cache.get_mut(&layer_id) {
if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) { if let Some(lane) = lanes.iter_mut().find(|l| l.node_id == node_id) {