egui_treeize/ui/
wire.rs

1use core::f32;
2
3use egui::{Context, Id, Pos2, Rect, Shape, Stroke, Ui, ahash::HashMap, cache::CacheTrait, pos2};
4
5use crate::{InPinId, OutPinId};
6
7const MAX_CURVE_SAMPLES: usize = 100;
8
9/// Layer where wires are rendered.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))]
13#[derive(Default)]
14pub enum WireLayer {
15  /// Wires are rendered behind nodes.
16  /// This is default.
17  #[default]
18  BehindNodes,
19
20  /// Wires are rendered above nodes.
21  AboveNodes,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub enum WireId {
26  Connected { treeize_id: Id, out_pin: OutPinId, in_pin: InPinId },
27  NewInput { treeize_id: Id, in_pin: InPinId },
28  NewOutput { treeize_id: Id, out_pin: OutPinId },
29}
30
31/// Controls style in which wire is rendered.
32///
33/// Variants are given in order of precedence when two pins require different styles.
34#[derive(Clone, Copy, Debug, PartialEq)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36#[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))]
37#[derive(Default)]
38pub enum WireStyle {
39  /// Straight line from one endpoint to another.
40  Line,
41
42  /// Draw wire as straight lines with 90 degree turns.
43  /// Corners has radius of `corner_radius`.
44  AxisAligned {
45    /// Radius of corners in wire.
46    corner_radius: f32,
47  },
48
49  /// Draw wire as 3rd degree Bezier curve.
50  Bezier3,
51
52  /// Draw wire as 5th degree Bezier curve.
53  #[default]
54  Bezier5,
55}
56
57pub const fn pick_wire_style(left: WireStyle, right: WireStyle) -> WireStyle {
58  match (left, right) {
59    (WireStyle::Line, _) | (_, WireStyle::Line) => WireStyle::Line,
60    (WireStyle::AxisAligned { corner_radius: a }, WireStyle::AxisAligned { corner_radius: b }) => {
61      WireStyle::AxisAligned { corner_radius: f32::max(a, b) }
62    }
63    (WireStyle::AxisAligned { corner_radius }, _)
64    | (_, WireStyle::AxisAligned { corner_radius }) => WireStyle::AxisAligned { corner_radius },
65    (WireStyle::Bezier3, _) | (_, WireStyle::Bezier3) => WireStyle::Bezier3,
66    (WireStyle::Bezier5, WireStyle::Bezier5) => WireStyle::Bezier5,
67  }
68}
69
70fn adjust_frame_size(
71  mut frame_size: f32,
72  upscale: bool,
73  downscale: bool,
74  from: Pos2,
75  to: Pos2,
76) -> f32 {
77  let length = (from - to).length();
78  if upscale {
79    frame_size = frame_size.max(length / 6.0);
80  }
81  if downscale {
82    frame_size = frame_size.min(length / 6.0);
83  }
84  frame_size
85}
86
87/// Returns 5th degree bezier curve control points for the wire
88fn wire_bezier_5(frame_size: f32, from: Pos2, to: Pos2) -> [Pos2; 6] {
89  let from_norm_x = frame_size;
90  let from_2 = pos2(from.x + from_norm_x, from.y);
91  let to_norm_x = -from_norm_x;
92  let to_2 = pos2(to.x + to_norm_x, to.y);
93
94  let between = (from_2 - to_2).length();
95
96  if from_2.x <= to_2.x && between >= frame_size * 2.0 {
97    let middle_1 = from_2 + (to_2 - from_2).normalized() * frame_size;
98    let middle_2 = to_2 + (from_2 - to_2).normalized() * frame_size;
99
100    [from, from_2, middle_1, middle_2, to_2, to]
101  } else if from_2.x <= to_2.x {
102    let t =
103      (between - (to_2.y - from_2.y).abs()) / frame_size.mul_add(2.0, -(to_2.y - from_2.y).abs());
104
105    let mut middle_1 = from_2 + (to_2 - from_2).normalized() * frame_size;
106    let mut middle_2 = to_2 + (from_2 - to_2).normalized() * frame_size;
107
108    if from_2.y >= to_2.y + frame_size {
109      let u = (from_2.y - to_2.y - frame_size) / frame_size;
110
111      let t0_middle_1 =
112        pos2((1.0 - u).mul_add(frame_size, from_2.x), frame_size.mul_add(-u, from_2.y));
113      let t0_middle_2 = pos2(to_2.x, to_2.y + frame_size);
114
115      middle_1 = t0_middle_1.lerp(middle_1, t);
116      middle_2 = t0_middle_2.lerp(middle_2, t);
117    } else if from_2.y >= to_2.y {
118      let u = (from_2.y - to_2.y) / frame_size;
119
120      let t0_middle_1 =
121        pos2(u.mul_add(frame_size, from_2.x), frame_size.mul_add(1.0 - u, from_2.y));
122      let t0_middle_2 = pos2(to_2.x, to_2.y + frame_size);
123
124      middle_1 = t0_middle_1.lerp(middle_1, t);
125      middle_2 = t0_middle_2.lerp(middle_2, t);
126    } else if to_2.y >= from_2.y + frame_size {
127      let u = (to_2.y - from_2.y - frame_size) / frame_size;
128
129      let t0_middle_1 = pos2(from_2.x, from_2.y + frame_size);
130      let t0_middle_2 =
131        pos2((1.0 - u).mul_add(-frame_size, to_2.x), frame_size.mul_add(-u, to_2.y));
132
133      middle_1 = t0_middle_1.lerp(middle_1, t);
134      middle_2 = t0_middle_2.lerp(middle_2, t);
135    } else if to_2.y >= from_2.y {
136      let u = (to_2.y - from_2.y) / frame_size;
137
138      let t0_middle_1 = pos2(from_2.x, from_2.y + frame_size);
139      let t0_middle_2 = pos2(u.mul_add(-frame_size, to_2.x), frame_size.mul_add(1.0 - u, to_2.y));
140
141      middle_1 = t0_middle_1.lerp(middle_1, t);
142      middle_2 = t0_middle_2.lerp(middle_2, t);
143    } else {
144      unreachable!();
145    }
146
147    [from, from_2, middle_1, middle_2, to_2, to]
148  } else if from_2.y >= frame_size.mul_add(2.0, to_2.y) {
149    let middle_1 = pos2(from_2.x, from_2.y - frame_size);
150    let middle_2 = pos2(to_2.x, to_2.y + frame_size);
151
152    [from, from_2, middle_1, middle_2, to_2, to]
153  } else if from_2.y >= to_2.y + frame_size {
154    let t = (from_2.y - to_2.y - frame_size) / frame_size;
155
156    let middle_1 = pos2((1.0 - t).mul_add(frame_size, from_2.x), frame_size.mul_add(-t, from_2.y));
157    let middle_2 = pos2(to_2.x, to_2.y + frame_size);
158
159    [from, from_2, middle_1, middle_2, to_2, to]
160  } else if from_2.y >= to_2.y {
161    let t = (from_2.y - to_2.y) / frame_size;
162
163    let middle_1 = pos2(t.mul_add(frame_size, from_2.x), frame_size.mul_add(1.0 - t, from_2.y));
164    let middle_2 = pos2(to_2.x, to_2.y + frame_size);
165
166    [from, from_2, middle_1, middle_2, to_2, to]
167  } else if to_2.y >= frame_size.mul_add(2.0, from_2.y) {
168    let middle_1 = pos2(from_2.x, from_2.y + frame_size);
169    let middle_2 = pos2(to_2.x, to_2.y - frame_size);
170
171    [from, from_2, middle_1, middle_2, to_2, to]
172  } else if to_2.y >= from_2.y + frame_size {
173    let t = (to_2.y - from_2.y - frame_size) / frame_size;
174
175    let middle_1 = pos2(from_2.x, from_2.y + frame_size);
176    let middle_2 = pos2((1.0 - t).mul_add(-frame_size, to_2.x), frame_size.mul_add(-t, to_2.y));
177
178    [from, from_2, middle_1, middle_2, to_2, to]
179  } else if to_2.y >= from_2.y {
180    let t = (to_2.y - from_2.y) / frame_size;
181
182    let middle_1 = pos2(from_2.x, from_2.y + frame_size);
183    let middle_2 = pos2(t.mul_add(-frame_size, to_2.x), frame_size.mul_add(1.0 - t, to_2.y));
184
185    [from, from_2, middle_1, middle_2, to_2, to]
186  } else {
187    unreachable!();
188  }
189}
190
191/// Returns 3rd degree bezier curve control points for the wire
192fn wire_bezier_3(frame_size: f32, from: Pos2, to: Pos2) -> [Pos2; 4] {
193  let [a, b, _, _, c, d] = wire_bezier_5(frame_size, from, to);
194  [a, b, c, d]
195}
196
197#[allow(clippy::too_many_arguments)]
198pub fn draw_wire(
199  ui: &Ui,
200  wire: WireId,
201  shapes: &mut Vec<Shape>,
202  frame_size: f32,
203  upscale: bool,
204  downscale: bool,
205  from: Pos2,
206  to: Pos2,
207  mut stroke: Stroke,
208  threshold: f32,
209  style: WireStyle,
210) {
211  if !ui.is_visible() {
212    return;
213  }
214
215  if stroke.width < 1.0 {
216    stroke.color = stroke.color.gamma_multiply(stroke.width);
217    stroke.width = 1.0;
218  }
219
220  let frame_size = adjust_frame_size(frame_size, upscale, downscale, from, to);
221
222  let args = WireArgs { frame_size, from, to, radius: 0.0 };
223
224  match style {
225    WireStyle::Line => {
226      let bb = Rect::from_two_pos(from, to);
227      if ui.is_rect_visible(bb) {
228        shapes.push(Shape::line_segment([from, to], stroke));
229      }
230    }
231    WireStyle::Bezier3 => {
232      draw_bezier_3(ui, wire, args, stroke, threshold, shapes);
233    }
234
235    WireStyle::Bezier5 => {
236      draw_bezier_5(ui, wire, args, stroke, threshold, shapes);
237    }
238
239    WireStyle::AxisAligned { corner_radius } => {
240      let args = WireArgs { radius: corner_radius, ..args };
241      draw_axis_aligned(ui, wire, args, stroke, threshold, shapes);
242    }
243  }
244}
245
246#[allow(clippy::too_many_arguments)]
247pub fn hit_wire(
248  ctx: &Context,
249  wire: WireId,
250  frame_size: f32,
251  upscale: bool,
252  downscale: bool,
253  from: Pos2,
254  to: Pos2,
255  pos: Pos2,
256  hit_threshold: f32,
257  style: WireStyle,
258) -> bool {
259  let frame_size = adjust_frame_size(frame_size, upscale, downscale, from, to);
260
261  let args = WireArgs { frame_size, from, to, radius: 0.0 };
262
263  match style {
264    WireStyle::Line => {
265      let aabb = Rect::from_two_pos(from, to);
266      let aabb_e = aabb.expand(hit_threshold);
267      if !aabb_e.contains(pos) {
268        return false;
269      }
270
271      let a = to - from;
272      let b = pos - from;
273
274      let dot = b.dot(a);
275      let dist2 = b.length_sq() - dot * dot / a.length_sq();
276
277      dist2 < hit_threshold * hit_threshold
278    }
279    WireStyle::Bezier3 => hit_wire_bezier_3(ctx, wire, args, pos, hit_threshold),
280    WireStyle::Bezier5 => hit_wire_bezier_5(ctx, wire, args, pos, hit_threshold),
281    WireStyle::AxisAligned { corner_radius } => {
282      let args = WireArgs { radius: corner_radius, ..args };
283      hit_wire_axis_aligned(ctx, wire, args, pos, hit_threshold)
284    }
285  }
286}
287
288#[inline]
289fn bezier_arc_length_upper_bound(points: &[Pos2]) -> f32 {
290  let mut size = 0.0;
291  for i in 1..points.len() {
292    size += (points[i] - points[i - 1]).length();
293  }
294  size
295}
296
297fn bezier_hit_samples_number(points: &[Pos2], threshold: f32) -> usize {
298  let arc_length = bezier_arc_length_upper_bound(points);
299
300  #[allow(clippy::cast_sign_loss)]
301  #[allow(clippy::cast_possible_truncation)]
302  ((arc_length / threshold).ceil().max(0.0) as usize)
303}
304
305fn bezier_derivative_3(points: &[Pos2; 4]) -> [Pos2; 3] {
306  let [p0, p1, p2, p3] = *points;
307
308  let factor = 3.0;
309
310  [(factor * (p1 - p0)).to_pos2(), (factor * (p2 - p1)).to_pos2(), (factor * (p3 - p2)).to_pos2()]
311}
312
313fn bezier_derivative_5(points: &[Pos2; 6]) -> [Pos2; 5] {
314  let [p0, p1, p2, p3, p4, p5] = *points;
315
316  let factor = 5.0;
317
318  [
319    (factor * (p1 - p0)).to_pos2(),
320    (factor * (p2 - p1)).to_pos2(),
321    (factor * (p3 - p2)).to_pos2(),
322    (factor * (p4 - p3)).to_pos2(),
323    (factor * (p5 - p4)).to_pos2(),
324  ]
325}
326
327fn bezier_draw_samples_number_3(points: &[Pos2; 4], threshold: f32) -> usize {
328  #![allow(clippy::similar_names)]
329  #![allow(clippy::cast_precision_loss)]
330
331  let d = bezier_derivative_3(points);
332
333  lower_bound(2, MAX_CURVE_SAMPLES, |n| {
334    let mut prev = points[0];
335    for i in 1..n {
336      let t = i as f32 / (n - 1) as f32;
337      let next = sample_bezier(points, t);
338
339      let m = t - 0.5 / (n - 1) as f32;
340
341      // Compare absolute error of mid point
342      let mid_line = ((prev.to_vec2() + next.to_vec2()) * 0.5).to_pos2();
343      let mid_curve = sample_bezier(points, m);
344
345      let error_sq = (mid_curve - mid_line).length_sq();
346      if error_sq > threshold * threshold {
347        return false;
348      }
349
350      // Compare angular error of mid point
351      let mid_line_dx = next.x - prev.x;
352      let mid_line_dy = next.y - prev.y;
353
354      let line_w = f32::hypot(mid_line_dx, mid_line_dy);
355
356      let d_curve = sample_bezier(&d, m);
357      let mid_curve_dx = d_curve.x;
358      let mid_curve_dy = d_curve.y;
359
360      let curve_w = f32::hypot(mid_curve_dx, mid_curve_dy);
361
362      let error = f32::max(
363        (mid_curve_dx / curve_w).mul_add(line_w, -mid_line_dx).abs(),
364        (mid_curve_dy / curve_w).mul_add(line_w, -mid_line_dy).abs(),
365      );
366      if error > threshold * 2.0 {
367        return false;
368      }
369
370      prev = next;
371    }
372
373    true
374  })
375}
376
377fn bezier_draw_samples_number_5(points: &[Pos2; 6], threshold: f32) -> usize {
378  #![allow(clippy::similar_names)]
379  #![allow(clippy::cast_precision_loss)]
380
381  let d = bezier_derivative_5(points);
382
383  lower_bound(2, MAX_CURVE_SAMPLES, |n| {
384    let mut prev = points[0];
385    for i in 1..n {
386      let t = i as f32 / (n - 1) as f32;
387      let next = sample_bezier(points, t);
388
389      let m = t - 0.5 / (n - 1) as f32;
390
391      // Compare absolute error of mid point
392      let mid_line = ((prev.to_vec2() + next.to_vec2()) * 0.5).to_pos2();
393      let mid_curve = sample_bezier(points, m);
394
395      let error_sq = (mid_curve - mid_line).length_sq();
396      if error_sq > threshold * threshold {
397        return false;
398      }
399
400      // Compare angular error of mid point
401      let mid_line_dx = next.x - prev.x;
402      let mid_line_dy = next.y - prev.y;
403
404      let line_w = f32::hypot(mid_line_dx, mid_line_dy);
405
406      let d_curve = sample_bezier(&d, m);
407      let mid_curve_dx = d_curve.x;
408      let mid_curve_dy = d_curve.y;
409
410      let curve_w = f32::hypot(mid_curve_dx, mid_curve_dy);
411
412      let error = f32::max(
413        (mid_curve_dx / curve_w).mul_add(line_w, -mid_line_dx).abs(),
414        (mid_curve_dy / curve_w).mul_add(line_w, -mid_line_dy).abs(),
415      );
416      if error > threshold * 2.0 {
417        return false;
418      }
419
420      prev = next;
421    }
422
423    true
424  })
425}
426
427#[derive(Clone, Copy, PartialEq)]
428struct WireArgs {
429  frame_size: f32,
430  from: Pos2,
431  to: Pos2,
432  radius: f32,
433}
434
435impl Default for WireArgs {
436  fn default() -> Self {
437    WireArgs { frame_size: 0.0, from: Pos2::ZERO, to: Pos2::ZERO, radius: 0.0 }
438  }
439}
440
441struct WireCache3 {
442  generation: u32,
443  args: WireArgs,
444  aabb: Rect,
445  points: [Pos2; 4],
446  threshold: f32,
447  line: Vec<Pos2>,
448}
449
450impl Default for WireCache3 {
451  fn default() -> Self {
452    WireCache3 {
453      generation: 0,
454      args: WireArgs::default(),
455      aabb: Rect::NOTHING,
456      points: [Pos2::ZERO; 4],
457      threshold: 0.0,
458      line: Vec::new(),
459    }
460  }
461}
462
463impl WireCache3 {
464  fn line(&mut self, threshold: f32) -> Vec<Pos2> {
465    #[allow(clippy::float_cmp)]
466    if !self.line.is_empty() && self.threshold == threshold {
467      return self.line.clone();
468    }
469
470    let samples = bezier_draw_samples_number_3(&self.points, threshold);
471
472    let line = (0..samples)
473      .map(|i| {
474        #[allow(clippy::cast_precision_loss)]
475        let t = i as f32 / (samples - 1) as f32;
476        sample_bezier(&self.points, t)
477      })
478      .collect::<Vec<Pos2>>();
479
480    self.threshold = threshold;
481    self.line.clone_from(&line);
482
483    line
484  }
485}
486
487struct WireCache5 {
488  generation: u32,
489  args: WireArgs,
490  aabb: Rect,
491  points: [Pos2; 6],
492  threshold: f32,
493  line: Vec<Pos2>,
494}
495
496impl Default for WireCache5 {
497  fn default() -> Self {
498    Self {
499      generation: 0,
500      args: WireArgs::default(),
501      aabb: Rect::NOTHING,
502      points: [Pos2::ZERO; 6],
503      threshold: 0.0,
504      line: Vec::new(),
505    }
506  }
507}
508
509impl WireCache5 {
510  fn line(&mut self, threshold: f32) -> Vec<Pos2> {
511    #[allow(clippy::float_cmp)]
512    if !self.line.is_empty() && self.threshold == threshold {
513      return self.line.clone();
514    }
515
516    let samples = bezier_draw_samples_number_5(&self.points, threshold);
517
518    let line = (0..samples)
519      .map(|i| {
520        #[allow(clippy::cast_precision_loss)]
521        let t = i as f32 / (samples - 1) as f32;
522        sample_bezier(&self.points, t)
523      })
524      .collect::<Vec<Pos2>>();
525
526    self.threshold = threshold;
527    self.line.clone_from(&line);
528
529    line
530  }
531}
532
533#[derive(Default)]
534struct WireCacheAA {
535  generation: u32,
536  args: WireArgs,
537  aawire: AxisAlignedWire,
538  threshold: f32,
539  line: Vec<Pos2>,
540}
541
542impl WireCacheAA {
543  fn line(&mut self, threshold: f32) -> Vec<Pos2> {
544    #[allow(clippy::float_cmp)]
545    if !self.line.is_empty() && self.threshold == threshold {
546      return self.line.clone();
547    }
548
549    let mut line = Vec::new();
550
551    for i in 0..self.aawire.turns {
552      // shapes.push(Shape::line_segment(
553      //     [wire.segments[i].0, wire.segments[i].1],
554      //     stroke,
555      // ));
556
557      // Draw segment first
558      line.push(self.aawire.segments[i].0);
559      line.push(self.aawire.segments[i].1);
560
561      if self.aawire.turn_radii[i] > 0.0 {
562        let turn = self.aawire.turn_centers[i];
563        let samples = turn_samples_number(self.aawire.turn_radii[i], self.threshold);
564
565        let start = self.aawire.segments[i].1;
566        let end = self.aawire.segments[i + 1].0;
567
568        let sin_x = end.x - turn.x;
569        let cos_x = start.x - turn.x;
570
571        let sin_y = end.y - turn.y;
572        let cos_y = start.y - turn.y;
573
574        for j in 1..samples {
575          #[allow(clippy::cast_precision_loss)]
576          let a = std::f32::consts::FRAC_PI_2 * (j as f32 / samples as f32);
577
578          let (sin_a, cos_a) = a.sin_cos();
579
580          let point: Pos2 = pos2(
581            cos_x.mul_add(cos_a, sin_x.mul_add(sin_a, turn.x)),
582            cos_y.mul_add(cos_a, sin_y.mul_add(sin_a, turn.y)),
583          );
584          line.push(point);
585        }
586      }
587    }
588
589    line.push(self.aawire.segments[self.aawire.turns].0);
590    line.push(self.aawire.segments[self.aawire.turns].1);
591
592    self.threshold = threshold;
593    self.line.clone_from(&line);
594
595    line
596  }
597}
598
599#[derive(Default)]
600struct WiresCache {
601  generation: u32,
602  bezier_3: HashMap<WireId, WireCache3>,
603  bezier_5: HashMap<WireId, WireCache5>,
604  axis_aligned: HashMap<WireId, WireCacheAA>,
605}
606
607impl CacheTrait for WiresCache {
608  fn update(&mut self) {
609    self.bezier_3.retain(|_, cache| cache.generation == self.generation);
610    self.bezier_5.retain(|_, cache| cache.generation == self.generation);
611    self.axis_aligned.retain(|_, cache| cache.generation == self.generation);
612
613    self.generation = self.generation.wrapping_add(1);
614  }
615
616  fn len(&self) -> usize {
617    self.bezier_3.len() + self.bezier_5.len() + self.axis_aligned.len()
618  }
619
620  fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
621    self
622  }
623}
624
625impl WiresCache {
626  pub fn get_3(&mut self, wire: WireId, args: WireArgs) -> &mut WireCache3 {
627    let cached = self.bezier_3.entry(wire).or_default();
628
629    cached.generation = self.generation;
630
631    if cached.args == args {
632      return cached;
633    }
634
635    let points = wire_bezier_3(args.frame_size, args.from, args.to);
636    let aabb = Rect::from_points(&points);
637
638    cached.args = args;
639    cached.points = points;
640    cached.aabb = aabb;
641    cached.line.clear();
642
643    cached
644  }
645
646  pub fn get_5(&mut self, wire: WireId, args: WireArgs) -> &mut WireCache5 {
647    let cached = self.bezier_5.entry(wire).or_default();
648
649    cached.generation = self.generation;
650
651    if cached.args == args {
652      return cached;
653    }
654
655    let points = wire_bezier_5(args.frame_size, args.from, args.to);
656    let aabb = Rect::from_points(&points);
657
658    cached.args = args;
659    cached.points = points;
660    cached.aabb = aabb;
661    cached.line.clear();
662
663    cached
664  }
665
666  #[allow(clippy::too_many_arguments)]
667  pub fn get_aa(&mut self, wire: WireId, args: WireArgs) -> &mut WireCacheAA {
668    let cached = self.axis_aligned.entry(wire).or_default();
669
670    cached.generation = self.generation;
671
672    if cached.args == args {
673      return cached;
674    }
675
676    let aawire = wire_axis_aligned(args.radius, args.frame_size, args.from, args.to);
677
678    cached.args = args;
679    cached.aawire = aawire;
680    cached.line.clear();
681
682    cached
683  }
684}
685
686#[inline(never)]
687fn draw_bezier_3(
688  ui: &Ui,
689  wire: WireId,
690  args: WireArgs,
691  stroke: Stroke,
692  threshold: f32,
693  shapes: &mut Vec<Shape>,
694) {
695  debug_assert!(ui.is_visible(), "Must be checked earlier");
696
697  let clip_rect = ui.clip_rect();
698
699  ui.memory_mut(|m| {
700    let cached = m.caches.cache::<WiresCache>().get_3(wire, args);
701
702    if cached.aabb.intersects(clip_rect) {
703      shapes.push(Shape::line(cached.line(threshold), stroke));
704    }
705  });
706
707  // {
708  //     let samples = bezier_draw_samples_number_3(points, threshold);
709  //     shapes.push(Shape::line(
710  //         points.to_vec(),
711  //         Stroke::new(1.0, Color32::PLACEHOLDER),
712  //     ));
713
714  //     let samples = 100;
715  //     shapes.push(Shape::line(
716  //         (0..samples)
717  //             .map(|i| {
718  //                 #[allow(clippy::cast_precision_loss)]
719  //                 let t = i as f32 / (samples - 1) as f32;
720  //                 sample_bezier(points, t)
721  //             })
722  //             .collect(),
723  //         Stroke::new(1.0, Color32::PLACEHOLDER),
724  //     ));
725  // }
726}
727
728fn draw_bezier_5(
729  ui: &Ui,
730  wire: WireId,
731  args: WireArgs,
732  stroke: Stroke,
733  threshold: f32,
734  shapes: &mut Vec<Shape>,
735) {
736  debug_assert!(ui.is_visible(), "Must be checked earlier");
737
738  let clip_rect = ui.clip_rect();
739
740  ui.memory_mut(|m| {
741    let cached = m.caches.cache::<WiresCache>().get_5(wire, args);
742
743    if cached.aabb.intersects(clip_rect) {
744      shapes.push(Shape::line(cached.line(threshold), stroke));
745    }
746  });
747
748  // {
749  //     let samples = bezier_draw_samples_number_5(points, threshold);
750  //     shapes.push(Shape::line(
751  //         points.to_vec(),
752  //         Stroke::new(1.0, Color32::PLACEHOLDER),
753  //     ));
754
755  //     let samples = 100;
756  //     shapes.push(Shape::line(
757  //         (0..samples)
758  //             .map(|i| {
759  //                 #[allow(clippy::cast_precision_loss)]
760  //                 let t = i as f32 / (samples - 1) as f32;
761  //                 sample_bezier(points, t)
762  //             })
763  //             .collect(),
764  //         Stroke::new(1.0, Color32::PLACEHOLDER),
765  //     ));
766  // }
767}
768
769// #[allow(clippy::let_and_return)]
770fn sample_bezier(points: &[Pos2], t: f32) -> Pos2 {
771  match *points {
772    [] => unimplemented!(),
773    [p0] => p0,
774    [p0, p1] => p0.lerp(p1, t),
775    [p0, p1, p2] => {
776      let p0_0 = p0;
777      let p1_0 = p1;
778      let p2_0 = p2;
779
780      let p0_1 = p0_0.lerp(p1_0, t);
781      let p1_1 = p1_0.lerp(p2_0, t);
782
783      p0_1.lerp(p1_1, t)
784    }
785    [p0, p1, p2, p3] => {
786      let p0_0 = p0;
787      let p1_0 = p1;
788      let p2_0 = p2;
789      let p3_0 = p3;
790
791      let p0_1 = p0_0.lerp(p1_0, t);
792      let p1_1 = p1_0.lerp(p2_0, t);
793      let p2_1 = p2_0.lerp(p3_0, t);
794
795      sample_bezier(&[p0_1, p1_1, p2_1], t)
796    }
797    [p0, p1, p2, p3, p4] => {
798      let p0_0 = p0;
799      let p1_0 = p1;
800      let p2_0 = p2;
801      let p3_0 = p3;
802      let p4_0 = p4;
803
804      let p0_1 = p0_0.lerp(p1_0, t);
805      let p1_1 = p1_0.lerp(p2_0, t);
806      let p2_1 = p2_0.lerp(p3_0, t);
807      let p3_1 = p3_0.lerp(p4_0, t);
808
809      sample_bezier(&[p0_1, p1_1, p2_1, p3_1], t)
810    }
811    [p0, p1, p2, p3, p4, p5] => {
812      let p0_0 = p0;
813      let p1_0 = p1;
814      let p2_0 = p2;
815      let p3_0 = p3;
816      let p4_0 = p4;
817      let p5_0 = p5;
818
819      let p0_1 = p0_0.lerp(p1_0, t);
820      let p1_1 = p1_0.lerp(p2_0, t);
821      let p2_1 = p2_0.lerp(p3_0, t);
822      let p3_1 = p3_0.lerp(p4_0, t);
823      let p4_1 = p4_0.lerp(p5_0, t);
824
825      sample_bezier(&[p0_1, p1_1, p2_1, p3_1, p4_1], t)
826    }
827    _ => unimplemented!(),
828  }
829}
830
831fn split_bezier_3(points: &[Pos2; 4], t: f32) -> [[Pos2; 4]; 2] {
832  let [p0, p1, p2, p3] = *points;
833
834  let p0_0 = p0;
835  let p1_0 = p1;
836  let p2_0 = p2;
837  let p3_0 = p3;
838
839  let p0_1 = p0_0.lerp(p1_0, t);
840  let p1_1 = p1_0.lerp(p2_0, t);
841  let p2_1 = p2_0.lerp(p3_0, t);
842
843  let p0_2 = p0_1.lerp(p1_1, t);
844  let p1_2 = p1_1.lerp(p2_1, t);
845
846  let p0_3 = p0_2.lerp(p1_2, t);
847
848  [[p0_0, p0_1, p0_2, p0_3], [p0_3, p1_2, p2_1, p3_0]]
849}
850
851fn hit_wire_bezier_3(
852  ctx: &Context,
853  wire: WireId,
854  args: WireArgs,
855  pos: Pos2,
856  hit_threshold: f32,
857) -> bool {
858  let (aabb, points) = ctx.memory_mut(|m| {
859    let cache = m.caches.cache::<WiresCache>().get_3(wire, args);
860
861    (cache.aabb, cache.points)
862  });
863
864  let aabb_e = aabb.expand(hit_threshold);
865  if !aabb_e.contains(pos) {
866    return false;
867  }
868
869  hit_bezier_3(&points, pos, hit_threshold)
870}
871
872fn hit_bezier_3(points: &[Pos2; 4], pos: Pos2, hit_threshold: f32) -> bool {
873  let samples = bezier_hit_samples_number(points, hit_threshold);
874  if samples > 8 {
875    let [points1, points2] = split_bezier_3(points, 0.5);
876
877    let aabb_e = Rect::from_points(&points1).expand(hit_threshold);
878    if aabb_e.contains(pos) && hit_bezier_3(&points1, pos, hit_threshold) {
879      return true;
880    }
881    let aabb_e = Rect::from_points(&points2).expand(hit_threshold);
882    if aabb_e.contains(pos) && hit_bezier_3(&points2, pos, hit_threshold) {
883      return true;
884    }
885    return false;
886  }
887
888  let threshold_sq = hit_threshold * hit_threshold;
889
890  for i in 0..samples {
891    #[allow(clippy::cast_precision_loss)]
892    let t = i as f32 / (samples - 1) as f32;
893    let p = sample_bezier(points, t);
894    if p.distance_sq(pos) <= threshold_sq {
895      return true;
896    }
897  }
898
899  false
900}
901
902fn split_bezier_5(points: &[Pos2; 6], t: f32) -> [[Pos2; 6]; 2] {
903  let [p0, p1, p2, p3, p4, p5] = *points;
904
905  let p0_0 = p0;
906  let p1_0 = p1;
907  let p2_0 = p2;
908  let p3_0 = p3;
909  let p4_0 = p4;
910  let p5_0 = p5;
911
912  let p0_1 = p0_0.lerp(p1_0, t);
913  let p1_1 = p1_0.lerp(p2_0, t);
914  let p2_1 = p2_0.lerp(p3_0, t);
915  let p3_1 = p3_0.lerp(p4_0, t);
916  let p4_1 = p4_0.lerp(p5_0, t);
917
918  let p0_2 = p0_1.lerp(p1_1, t);
919  let p1_2 = p1_1.lerp(p2_1, t);
920  let p2_2 = p2_1.lerp(p3_1, t);
921  let p3_2 = p3_1.lerp(p4_1, t);
922
923  let p0_3 = p0_2.lerp(p1_2, t);
924  let p1_3 = p1_2.lerp(p2_2, t);
925  let p2_3 = p2_2.lerp(p3_2, t);
926
927  let p0_4 = p0_3.lerp(p1_3, t);
928  let p1_4 = p1_3.lerp(p2_3, t);
929
930  let p0_5 = p0_4.lerp(p1_4, t);
931
932  [[p0_0, p0_1, p0_2, p0_3, p0_4, p0_5], [p0_5, p1_4, p2_3, p3_2, p4_1, p5_0]]
933}
934
935fn hit_wire_bezier_5(
936  ctx: &Context,
937  wire: WireId,
938  args: WireArgs,
939  pos: Pos2,
940  hit_threshold: f32,
941) -> bool {
942  let (aabb, points) = ctx.memory_mut(|m| {
943    let cache = m.caches.cache::<WiresCache>().get_5(wire, args);
944
945    (cache.aabb, cache.points)
946  });
947
948  let aabb_e = aabb.expand(hit_threshold);
949  if !aabb_e.contains(pos) {
950    return false;
951  }
952
953  hit_bezier_5(&points, pos, hit_threshold)
954}
955
956fn hit_bezier_5(points: &[Pos2; 6], pos: Pos2, hit_threshold: f32) -> bool {
957  let samples = bezier_hit_samples_number(points, hit_threshold);
958  if samples > 16 {
959    let [points1, points2] = split_bezier_5(points, 0.5);
960    let aabb_e = Rect::from_points(&points1).expand(hit_threshold);
961    if aabb_e.contains(pos) && hit_bezier_5(&points1, pos, hit_threshold) {
962      return true;
963    }
964    let aabb_e = Rect::from_points(&points2).expand(hit_threshold);
965    if aabb_e.contains(pos) && hit_bezier_5(&points2, pos, hit_threshold) {
966      return true;
967    }
968    return false;
969  }
970
971  let threshold_sq = hit_threshold * hit_threshold;
972
973  for i in 0..samples {
974    #[allow(clippy::cast_precision_loss)]
975    let t = i as f32 / (samples - 1) as f32;
976    let p = sample_bezier(points, t);
977
978    if p.distance_sq(pos) <= threshold_sq {
979      return true;
980    }
981  }
982
983  false
984}
985
986#[derive(Clone, Copy, PartialEq)]
987struct AxisAlignedWire {
988  aabb: Rect,
989  turns: usize,
990  segments: [(Pos2, Pos2); 5],
991  turn_centers: [Pos2; 4],
992  turn_radii: [f32; 4],
993}
994
995impl Default for AxisAlignedWire {
996  #[inline]
997  fn default() -> Self {
998    Self {
999      aabb: Rect::NOTHING,
1000      turns: 0,
1001      segments: [(Pos2::ZERO, Pos2::ZERO); 5],
1002      turn_centers: [Pos2::ZERO; 4],
1003      turn_radii: [0.0; 4],
1004    }
1005  }
1006}
1007
1008#[allow(clippy::too_many_lines)]
1009fn wire_axis_aligned(corner_radius: f32, frame_size: f32, from: Pos2, to: Pos2) -> AxisAlignedWire {
1010  let corner_radius = corner_radius.max(0.0);
1011
1012  let half_width = f32::abs(from.x - to.x) / 2.0;
1013  let max_radius = (half_width / 2.0).min(corner_radius);
1014
1015  let frame_size = frame_size.max(max_radius * 2.0);
1016
1017  let zero_segment = (Pos2::ZERO, Pos2::ZERO);
1018
1019  if from.y + frame_size <= to.y - frame_size {
1020    if f32::abs(from.x - to.x) < 1.0 {
1021      // Single segment case.
1022      AxisAlignedWire {
1023        aabb: Rect::from_two_pos(from, to),
1024        segments: [(from, to), zero_segment, zero_segment, zero_segment, zero_segment],
1025        turns: 0,
1026        turn_centers: [Pos2::ZERO; 4],
1027        turn_radii: [f32::NAN; 4],
1028      }
1029    } else {
1030      // Two turns case.
1031      let mid_y = f32::midpoint(from.y, to.y);
1032      let half_height = (to.y - from.y) / 2.0;
1033
1034      let turn_radius = max_radius.min(half_height);
1035
1036      let turn_horz_len = if from.x < to.x { turn_radius } else { -turn_radius };
1037
1038      let segments = [
1039        (from, pos2(from.x, mid_y - turn_radius)),
1040        (pos2(from.x + turn_horz_len, mid_y), pos2(to.x - turn_horz_len, mid_y)),
1041        (pos2(to.x, mid_y + turn_radius), to),
1042        zero_segment,
1043        zero_segment,
1044      ];
1045
1046      let turn_centers = [
1047        pos2(from.x + turn_horz_len, mid_y - turn_radius),
1048        pos2(to.x - turn_horz_len, mid_y + turn_radius),
1049        Pos2::ZERO,
1050        Pos2::ZERO,
1051      ];
1052
1053      let turn_radii = [turn_radius, turn_radius, f32::NAN, f32::NAN];
1054
1055      AxisAlignedWire {
1056        aabb: Rect::from_two_pos(from, to),
1057        turns: 2,
1058        segments,
1059        turn_centers,
1060        turn_radii,
1061      }
1062    }
1063  } else {
1064    // Four turns case.
1065    let mid = f32::midpoint(from.x, to.x);
1066
1067    let bottom = from.y + frame_size;
1068    let top = to.y - frame_size;
1069
1070    let half_height = f32::abs(bottom - top) / 2.0;
1071
1072    let ends_turn_radius = max_radius;
1073    let middle_turn_radius = max_radius.min(half_height);
1074
1075    let ends_turn_horz_len = if from.x < to.x { ends_turn_radius } else { -ends_turn_radius };
1076
1077    let middle_turn_horz_len = if from.x < to.x { middle_turn_radius } else { -middle_turn_radius };
1078
1079    let segments = [
1080      (from, pos2(from.x, bottom - ends_turn_radius)),
1081      (pos2(from.x + ends_turn_horz_len, bottom), pos2(mid - middle_turn_horz_len, bottom)),
1082      (pos2(mid, bottom - middle_turn_radius), pos2(mid, top + middle_turn_radius)),
1083      (pos2(mid + middle_turn_horz_len, top), pos2(to.x - ends_turn_horz_len, top)),
1084      (pos2(to.x, top + ends_turn_radius), to),
1085    ];
1086
1087    let turn_centers = [
1088      pos2(from.x + ends_turn_horz_len, bottom - ends_turn_radius),
1089      pos2(mid - middle_turn_horz_len, bottom - middle_turn_radius),
1090      pos2(mid + middle_turn_horz_len, top + middle_turn_radius),
1091      pos2(to.x - ends_turn_horz_len, top + ends_turn_radius),
1092    ];
1093
1094    let turn_radii = [ends_turn_radius, middle_turn_radius, middle_turn_radius, ends_turn_radius];
1095
1096    AxisAlignedWire {
1097      aabb: Rect::from_min_max(
1098        pos2(f32::min(from.x, to.x), f32::min(top, from.y)),
1099        pos2(f32::max(from.x, to.x), f32::max(bottom, to.y)),
1100      ),
1101      turns: 4,
1102      segments,
1103      turn_centers,
1104      turn_radii,
1105    }
1106  }
1107}
1108
1109fn hit_wire_axis_aligned(
1110  ctx: &Context,
1111  wire: WireId,
1112  args: WireArgs,
1113  pos: Pos2,
1114  hit_threshold: f32,
1115) -> bool {
1116  let aawire = ctx.memory_mut(|m| {
1117    let cache = m.caches.cache::<WiresCache>().get_aa(wire, args);
1118
1119    cache.aawire
1120  });
1121
1122  // Check AABB first
1123  if !aawire.aabb.expand(hit_threshold).contains(pos) {
1124    return false;
1125  }
1126
1127  // Check all straight segments first
1128  // Number of segments is number of turns + 1
1129  for i in 0..aawire.turns + 1 {
1130    let (start, end) = aawire.segments[i];
1131
1132    // Segments are always axis aligned
1133    // So we can use AABB for checking
1134    if Rect::from_two_pos(start, end).expand(hit_threshold).contains(pos) {
1135      return true;
1136    }
1137  }
1138
1139  // Check all turns
1140  for i in 0..aawire.turns {
1141    if aawire.turn_radii[i] > 0.0 {
1142      let turn = aawire.turn_centers[i];
1143      let turn_aabb = Rect::from_two_pos(aawire.segments[i].1, aawire.segments[i + 1].0);
1144      if !turn_aabb.contains(pos) {
1145        continue;
1146      }
1147
1148      // Avoid sqrt
1149      let dist2 = (turn - pos).length_sq();
1150      let min = aawire.turn_radii[i] - hit_threshold;
1151      let max = aawire.turn_radii[i] + hit_threshold;
1152
1153      if dist2 <= max * max && dist2 >= min * min {
1154        return true;
1155      }
1156    }
1157  }
1158
1159  false
1160}
1161
1162fn turn_samples_number(radius: f32, threshold: f32) -> usize {
1163  #![allow(clippy::cast_sign_loss)]
1164  #![allow(clippy::cast_possible_truncation)]
1165  #![allow(clippy::cast_precision_loss)]
1166
1167  if threshold / radius >= 1.0 {
1168    return 2;
1169  }
1170
1171  let a: f32 = (1.0 - threshold / radius).acos();
1172  let samples =
1173    (std::f32::consts::PI / (4.0 * a) + 1.0).min(MAX_CURVE_SAMPLES as f32).ceil() as usize;
1174
1175  samples.clamp(2, MAX_CURVE_SAMPLES)
1176}
1177
1178#[allow(clippy::too_many_arguments)]
1179fn draw_axis_aligned(
1180  ui: &Ui,
1181  wire: WireId,
1182  args: WireArgs,
1183  stroke: Stroke,
1184  threshold: f32,
1185  shapes: &mut Vec<Shape>,
1186) {
1187  debug_assert!(ui.is_visible(), "Must be checked earlier");
1188
1189  let clip_rect = ui.clip_rect();
1190  ui.memory_mut(|m| {
1191    let cached = m.caches.cache::<WiresCache>().get_aa(wire, args);
1192
1193    if cached.aawire.aabb.intersects(clip_rect) {
1194      shapes.push(Shape::line(cached.line(threshold), stroke));
1195    }
1196  });
1197}
1198
1199/// Very basic lower-bound algorithm
1200/// Finds the smallest number in range [min, max) that satisfies the predicate
1201/// If no such number exists, returns max
1202///
1203/// For the algorithm to work, the predicate must be monotonic
1204/// i.e. if f(i) is true, then f(j) is true for all j within (i, max)
1205/// and if f(i) is false, then f(j) is false for all j within [min, i)
1206fn lower_bound(min: usize, max: usize, f: impl Fn(usize) -> bool) -> usize {
1207  #![allow(clippy::similar_names)]
1208
1209  let mut min = min;
1210  let mut max = max;
1211
1212  while min < max {
1213    let mid = usize::midpoint(min, max);
1214    if f(mid) {
1215      max = mid;
1216    } else {
1217      min = mid + 1;
1218    }
1219  }
1220
1221  max
1222
1223  // for i in min..max {
1224  //     if f(i) {
1225  //         return i;
1226  //     }
1227  // }
1228  // max
1229}