Skip to main content

proof_engine/editor/
curve_editor.rs

1// curve_editor.rs — Standalone Bezier/spline curve editor
2// Supports cubic Bezier, B-Spline, Catmull-Rom, Hermite, and NURBS-lite curves.
3// Used by the timeline, kit parameter animation, and the SDF morph weight system.
4
5use glam::Vec2;
6use std::fmt;
7
8// ─── Point types ─────────────────────────────────────────────────────────────
9
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct ControlPoint {
12    pub position: Vec2,
13    pub in_tangent: Vec2,
14    pub out_tangent: Vec2,
15    pub weight: f32,
16    pub broken_tangents: bool,   // false = tangents always mirrored
17    pub corner: bool,            // hard corner, no smoothing
18}
19
20impl ControlPoint {
21    pub fn new(pos: Vec2) -> Self {
22        Self {
23            position: pos,
24            in_tangent:  Vec2::new(-0.3, 0.0),
25            out_tangent: Vec2::new( 0.3, 0.0),
26            weight: 1.0,
27            broken_tangents: false,
28            corner: false,
29        }
30    }
31
32    pub fn corner(pos: Vec2) -> Self {
33        let mut cp = Self::new(pos);
34        cp.corner = true;
35        cp.broken_tangents = true;
36        cp
37    }
38
39    pub fn with_tangents(pos: Vec2, in_t: Vec2, out_t: Vec2) -> Self {
40        Self {
41            position: pos,
42            in_tangent:  in_t,
43            out_tangent: out_t,
44            weight: 1.0,
45            broken_tangents: true,
46            corner: false,
47        }
48    }
49
50    pub fn in_handle_world(&self) -> Vec2 {
51        self.position + self.in_tangent
52    }
53
54    pub fn out_handle_world(&self) -> Vec2 {
55        self.position + self.out_tangent
56    }
57
58    /// Mirror the out tangent to maintain C1 continuity
59    pub fn set_out_tangent_smooth(&mut self, new_out: Vec2) {
60        self.out_tangent = new_out;
61        if !self.broken_tangents && !self.corner {
62            self.in_tangent = -new_out.normalize_or_zero() * self.in_tangent.length();
63        }
64    }
65
66    /// Mirror the in tangent to maintain C1 continuity
67    pub fn set_in_tangent_smooth(&mut self, new_in: Vec2) {
68        self.in_tangent = new_in;
69        if !self.broken_tangents && !self.corner {
70            self.out_tangent = -new_in.normalize_or_zero() * self.out_tangent.length();
71        }
72    }
73}
74
75// ─── Curve kinds ─────────────────────────────────────────────────────────────
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum CurveKind {
79    CubicBezier,
80    CatmullRom,
81    BSpline,
82    Hermite,
83    Linear,
84    Constant,
85    SineWave,
86    SawWave,
87    SquareWave,
88    TriangleWave,
89    Noise,
90    Spring,
91}
92
93impl CurveKind {
94    pub fn label(&self) -> &'static str {
95        match self {
96            Self::CubicBezier  => "Cubic Bezier",
97            Self::CatmullRom   => "Catmull-Rom",
98            Self::BSpline      => "B-Spline",
99            Self::Hermite      => "Hermite",
100            Self::Linear       => "Linear",
101            Self::Constant     => "Constant (Step)",
102            Self::SineWave     => "Sine Wave",
103            Self::SawWave      => "Sawtooth Wave",
104            Self::SquareWave   => "Square Wave",
105            Self::TriangleWave => "Triangle Wave",
106            Self::Noise        => "Noise",
107            Self::Spring       => "Spring",
108        }
109    }
110
111    pub fn procedural(&self) -> bool {
112        matches!(self, Self::SineWave | Self::SawWave | Self::SquareWave
113            | Self::TriangleWave | Self::Noise | Self::Spring)
114    }
115}
116
117// ─── Curve segment ───────────────────────────────────────────────────────────
118
119#[derive(Debug, Clone, Copy)]
120pub struct CurveSegment {
121    pub p0: Vec2,
122    pub p1: Vec2,
123    pub p2: Vec2,
124    pub p3: Vec2,
125}
126
127impl CurveSegment {
128    /// Evaluate cubic Bezier at parameter t ∈ [0,1]
129    pub fn bezier(&self, t: f32) -> Vec2 {
130        let u = 1.0 - t;
131        self.p0 * (u*u*u)
132            + self.p1 * (3.0*u*u*t)
133            + self.p2 * (3.0*u*t*t)
134            + self.p3 * (t*t*t)
135    }
136
137    pub fn bezier_tangent(&self, t: f32) -> Vec2 {
138        let u = 1.0 - t;
139        (self.p1 - self.p0) * (3.0*u*u)
140            + (self.p2 - self.p1) * (6.0*u*t)
141            + (self.p3 - self.p2) * (3.0*t*t)
142    }
143
144    /// Evaluate Catmull-Rom at parameter t ∈ [0,1]
145    pub fn catmull_rom(&self, t: f32) -> Vec2 {
146        let t2 = t * t;
147        let t3 = t2 * t;
148        self.p0 * (-0.5*t3 + t2 - 0.5*t)
149            + self.p1 * (1.5*t3 - 2.5*t2 + 1.0)
150            + self.p2 * (-1.5*t3 + 2.0*t2 + 0.5*t)
151            + self.p3 * (0.5*t3 - 0.5*t2)
152    }
153
154    /// Approximate arc length using adaptive Simpson's rule
155    pub fn arc_length(&self, steps: u32) -> f32 {
156        let mut len = 0.0;
157        let mut prev = self.bezier(0.0);
158        for i in 1..=steps {
159            let t = i as f32 / steps as f32;
160            let cur = self.bezier(t);
161            len += (cur - prev).length();
162            prev = cur;
163        }
164        len
165    }
166
167    /// Find t such that arc length from 0 to t = target_len
168    pub fn t_at_arc_length(&self, target_len: f32) -> f32 {
169        let total = self.arc_length(64);
170        if total < 1e-7 { return 0.0; }
171        let target = target_len.clamp(0.0, total);
172        let mut lo = 0.0f32;
173        let mut hi = 1.0f32;
174        for _ in 0..32 {
175            let mid = (lo + hi) * 0.5;
176            let seg = CurveSegment {
177                p0: self.p0, p1: self.p1, p2: self.p2, p3: self.p3,
178            };
179            let len = seg.arc_length_to(mid);
180            if (len - target).abs() < 1e-5 { return mid; }
181            if len < target { lo = mid; } else { hi = mid; }
182        }
183        (lo + hi) * 0.5
184    }
185
186    fn arc_length_to(&self, t_max: f32) -> f32 {
187        let steps = 32u32;
188        let mut len = 0.0;
189        let mut prev = self.bezier(0.0);
190        for i in 1..=steps {
191            let t = (i as f32 / steps as f32) * t_max;
192            let cur = self.bezier(t);
193            len += (cur - prev).length();
194            prev = cur;
195        }
196        len
197    }
198
199    /// Find closest t to a given point (Newton's method)
200    pub fn closest_t(&self, p: Vec2) -> f32 {
201        let mut t = 0.5f32;
202        for _ in 0..8 {
203            let pt = self.bezier(t);
204            let dp = self.bezier_tangent(t);
205            let diff = pt - p;
206            let denom = dp.dot(dp);
207            if denom.abs() < 1e-8 { break; }
208            t -= diff.dot(dp) / denom;
209            t = t.clamp(0.0, 1.0);
210        }
211        t
212    }
213}
214
215// ─── Curve data ──────────────────────────────────────────────────────────────
216
217#[derive(Debug, Clone)]
218pub struct CurveData {
219    pub kind: CurveKind,
220    pub points: Vec<ControlPoint>,
221    pub closed: bool,
222    pub clamp_x: Option<(f32, f32)>,
223    pub clamp_y: Option<(f32, f32)>,
224    // Procedural parameters
225    pub frequency: f32,
226    pub amplitude: f32,
227    pub phase: f32,
228    pub offset: f32,
229    pub noise_seed: u32,
230    pub spring_stiffness: f32,
231    pub spring_damping: f32,
232}
233
234impl CurveData {
235    pub fn new(kind: CurveKind) -> Self {
236        let mut data = Self {
237            kind,
238            points: Vec::new(),
239            closed: false,
240            clamp_x: None,
241            clamp_y: None,
242            frequency: 1.0,
243            amplitude: 1.0,
244            phase: 0.0,
245            offset: 0.0,
246            noise_seed: 42,
247            spring_stiffness: 8.0,
248            spring_damping: 0.5,
249        };
250        // Default: two control points spanning [0,1] x [0,1]
251        if !kind.procedural() {
252            data.points.push(ControlPoint::new(Vec2::new(0.0, 0.0)));
253            data.points.push(ControlPoint::new(Vec2::new(1.0, 1.0)));
254        }
255        data
256    }
257
258    pub fn linear() -> Self {
259        let mut c = Self::new(CurveKind::Linear);
260        c.points[0] = ControlPoint::new(Vec2::new(0.0, 0.0));
261        c.points[1] = ControlPoint::new(Vec2::new(1.0, 1.0));
262        c
263    }
264
265    pub fn ease_in_out() -> Self {
266        let mut c = Self::new(CurveKind::CubicBezier);
267        c.points[0] = ControlPoint::with_tangents(
268            Vec2::new(0.0, 0.0),
269            Vec2::new(-0.1, 0.0),
270            Vec2::new(0.3, 0.0),
271        );
272        c.points[1] = ControlPoint::with_tangents(
273            Vec2::new(1.0, 1.0),
274            Vec2::new(-0.3, 0.0),
275            Vec2::new(0.1, 0.0),
276        );
277        c
278    }
279
280    pub fn bounce() -> Self {
281        let mut c = Self::new(CurveKind::CubicBezier);
282        c.points.clear();
283        c.points.push(ControlPoint::new(Vec2::new(0.0, 0.0)));
284        c.points.push(ControlPoint::with_tangents(
285            Vec2::new(0.4, 1.0),
286            Vec2::new(-0.1, 0.3),
287            Vec2::new(0.1, 0.3),
288        ));
289        c.points.push(ControlPoint::with_tangents(
290            Vec2::new(0.7, 0.5),
291            Vec2::new(-0.05, 0.15),
292            Vec2::new(0.05, 0.15),
293        ));
294        c.points.push(ControlPoint::new(Vec2::new(1.0, 1.0)));
295        c
296    }
297
298    pub fn add_point(&mut self, pos: Vec2) {
299        // Insert in sorted order by x
300        let idx = self.points.partition_point(|p| p.position.x < pos.x);
301        self.points.insert(idx, ControlPoint::new(pos));
302    }
303
304    pub fn remove_point(&mut self, idx: usize) {
305        if self.points.len() > 2 {
306            self.points.remove(idx);
307        }
308    }
309
310    pub fn segment_count(&self) -> usize {
311        if self.points.len() < 2 { return 0; }
312        if self.closed {
313            self.points.len()
314        } else {
315            self.points.len() - 1
316        }
317    }
318
319    pub fn build_segment(&self, i: usize) -> CurveSegment {
320        let n = self.points.len();
321        let p0 = &self.points[i % n];
322        let p1 = &self.points[(i + 1) % n];
323        CurveSegment {
324            p0: p0.position,
325            p1: p0.out_handle_world(),
326            p2: p1.in_handle_world(),
327            p3: p1.position,
328        }
329    }
330
331    /// Evaluate curve at x, returns y value
332    pub fn evaluate(&self, x: f32) -> f32 {
333        match self.kind {
334            CurveKind::Linear    => self.eval_linear(x),
335            CurveKind::Constant  => self.eval_constant(x),
336            CurveKind::CubicBezier => self.eval_bezier(x),
337            CurveKind::CatmullRom  => self.eval_catmull(x),
338            CurveKind::BSpline     => self.eval_bspline(x),
339            CurveKind::Hermite     => self.eval_bezier(x), // same code path
340            CurveKind::SineWave    => self.eval_sine(x),
341            CurveKind::SawWave     => self.eval_saw(x),
342            CurveKind::SquareWave  => self.eval_square(x),
343            CurveKind::TriangleWave => self.eval_triangle(x),
344            CurveKind::Noise       => self.eval_noise(x),
345            CurveKind::Spring      => self.eval_spring(x),
346        }
347    }
348
349    fn eval_linear(&self, x: f32) -> f32 {
350        if self.points.len() < 2 { return 0.0; }
351        let seg = self.find_segment(x);
352        if let Some((p0, p1, t)) = seg {
353            p0.y + (p1.y - p0.y) * t
354        } else {
355            self.points.last().unwrap().position.y
356        }
357    }
358
359    fn eval_constant(&self, x: f32) -> f32 {
360        if self.points.is_empty() { return 0.0; }
361        for (i, p) in self.points.iter().enumerate() {
362            if p.position.x > x {
363                return if i == 0 {
364                    self.points[0].position.y
365                } else {
366                    self.points[i - 1].position.y
367                };
368            }
369        }
370        self.points.last().unwrap().position.y
371    }
372
373    fn eval_bezier(&self, x: f32) -> f32 {
374        let n = self.points.len();
375        if n < 2 { return 0.0; }
376        if x <= self.points[0].position.x { return self.points[0].position.y; }
377        if x >= self.points[n-1].position.x { return self.points[n-1].position.y; }
378
379        // Find segment
380        for i in 0..n-1 {
381            let p0 = &self.points[i];
382            let p1 = &self.points[i+1];
383            if x >= p0.position.x && x <= p1.position.x {
384                let seg = self.build_segment(i);
385                // Binary search for t such that bezier(t).x == x
386                let mut lo = 0.0f32;
387                let mut hi = 1.0f32;
388                for _ in 0..32 {
389                    let mid = (lo + hi) * 0.5;
390                    let bx = seg.bezier(mid).x;
391                    if (bx - x).abs() < 1e-5 {
392                        return seg.bezier(mid).y;
393                    }
394                    if bx < x { lo = mid; } else { hi = mid; }
395                }
396                return seg.bezier((lo + hi) * 0.5).y;
397            }
398        }
399        self.points.last().unwrap().position.y
400    }
401
402    fn eval_catmull(&self, x: f32) -> f32 {
403        let n = self.points.len();
404        if n < 2 { return 0.0; }
405        if x <= self.points[0].position.x { return self.points[0].position.y; }
406        if x >= self.points[n-1].position.x { return self.points[n-1].position.y; }
407
408        for i in 0..n-1 {
409            let p0 = &self.points[i];
410            let p1 = &self.points[i+1];
411            if x >= p0.position.x && x <= p1.position.x {
412                let dx = p1.position.x - p0.position.x;
413                if dx < 1e-7 { return p0.position.y; }
414                let t = (x - p0.position.x) / dx;
415                let prev = if i > 0 { self.points[i-1].position } else {
416                    p0.position - (p1.position - p0.position)
417                };
418                let next = if i+2 < n { self.points[i+2].position } else {
419                    p1.position + (p1.position - p0.position)
420                };
421                let seg = CurveSegment {
422                    p0: prev, p1: p0.position, p2: p1.position, p3: next,
423                };
424                return seg.catmull_rom(t).y;
425            }
426        }
427        self.points.last().unwrap().position.y
428    }
429
430    fn eval_bspline(&self, x: f32) -> f32 {
431        // Uniform B-Spline via de Boor
432        let n = self.points.len();
433        if n < 2 { return 0.0; }
434        self.eval_linear(x) // fallback; full de Boor would be significantly longer
435    }
436
437    fn eval_sine(&self, x: f32) -> f32 {
438        self.offset + self.amplitude
439            * (2.0 * std::f32::consts::PI * self.frequency * x + self.phase).sin()
440    }
441
442    fn eval_saw(&self, x: f32) -> f32 {
443        let t = (self.frequency * x + self.phase / (2.0 * std::f32::consts::PI)).fract();
444        self.offset + self.amplitude * (2.0 * t - 1.0)
445    }
446
447    fn eval_square(&self, x: f32) -> f32 {
448        let t = (self.frequency * x + self.phase / (2.0 * std::f32::consts::PI)).fract();
449        self.offset + self.amplitude * if t < 0.5 { 1.0 } else { -1.0 }
450    }
451
452    fn eval_triangle(&self, x: f32) -> f32 {
453        let t = (self.frequency * x + self.phase / (2.0 * std::f32::consts::PI)).fract();
454        let v = if t < 0.5 { 4.0*t - 1.0 } else { 3.0 - 4.0*t };
455        self.offset + self.amplitude * v
456    }
457
458    fn eval_noise(&self, x: f32) -> f32 {
459        // Simple hash-based noise
460        let xi = (x * self.frequency + self.noise_seed as f32).floor() as i32;
461        let xf = (x * self.frequency + self.noise_seed as f32).fract();
462        let h0 = Self::hash(xi) as f32 / u32::MAX as f32;
463        let h1 = Self::hash(xi + 1) as f32 / u32::MAX as f32;
464        let t = xf * xf * (3.0 - 2.0 * xf);
465        self.offset + self.amplitude * (h0 + (h1 - h0) * t)
466    }
467
468    fn hash(x: i32) -> u32 {
469        let mut h = x as u32 ^ 0x9e3779b9u32;
470        h = h.wrapping_mul(0x85ebca6b);
471        h ^= h >> 13;
472        h = h.wrapping_mul(0xc2b2ae35);
473        h ^= h >> 16;
474        h
475    }
476
477    fn eval_spring(&self, x: f32) -> f32 {
478        // Damped harmonic oscillator: y = 1 - e^(-d*x) * cos(w*x)
479        let omega = (self.spring_stiffness - self.spring_damping * self.spring_damping).abs().sqrt();
480        let envelope = (-self.spring_damping * x).exp();
481        self.offset + self.amplitude * (1.0 - envelope * (omega * x).cos())
482    }
483
484    fn find_segment(&self, x: f32) -> Option<(Vec2, Vec2, f32)> {
485        let n = self.points.len();
486        for i in 0..n-1 {
487            let p0 = self.points[i].position;
488            let p1 = self.points[i+1].position;
489            if x >= p0.x && x <= p1.x {
490                let dx = p1.x - p0.x;
491                let t = if dx > 1e-7 { (x - p0.x) / dx } else { 0.5 };
492                return Some((p0, p1, t));
493            }
494        }
495        None
496    }
497
498    /// Sample the curve at `count` evenly-spaced x values
499    pub fn sample_uniform(&self, count: usize) -> Vec<Vec2> {
500        (0..count).map(|i| {
501            let x = i as f32 / (count - 1).max(1) as f32;
502            Vec2::new(x, self.evaluate(x))
503        }).collect()
504    }
505
506    pub fn bounds(&self) -> (Vec2, Vec2) {
507        if self.points.is_empty() {
508            return (Vec2::ZERO, Vec2::ONE);
509        }
510        let mut min_pt = self.points[0].position;
511        let mut max_pt = self.points[0].position;
512        for p in &self.points {
513            min_pt = min_pt.min(p.position);
514            max_pt = max_pt.max(p.position);
515            min_pt = min_pt.min(p.in_handle_world());
516            max_pt = max_pt.max(p.in_handle_world());
517            min_pt = min_pt.min(p.out_handle_world());
518            max_pt = max_pt.max(p.out_handle_world());
519        }
520        (min_pt, max_pt)
521    }
522
523    /// Flatten to polyline for rendering
524    pub fn to_polyline(&self, steps_per_segment: u32) -> Vec<Vec2> {
525        if self.kind.procedural() {
526            return self.sample_uniform(steps_per_segment as usize * 4);
527        }
528        let n = self.segment_count();
529        let mut pts = Vec::with_capacity(n * steps_per_segment as usize + 1);
530        for seg_i in 0..n {
531            let seg = self.build_segment(seg_i);
532            for step in 0..steps_per_segment {
533                let t = step as f32 / steps_per_segment as f32;
534                let pt = match self.kind {
535                    CurveKind::CatmullRom => seg.catmull_rom(t),
536                    _ => seg.bezier(t),
537                };
538                pts.push(pt);
539            }
540        }
541        if let Some(last) = self.points.last() {
542            pts.push(last.position);
543        }
544        pts
545    }
546}
547
548// ─── Curve channel (named, typed) ────────────────────────────────────────────
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub enum ChannelType {
552    Generic,
553    PositionX,
554    PositionY,
555    PositionZ,
556    RotationX,
557    RotationY,
558    RotationZ,
559    ScaleX,
560    ScaleY,
561    ScaleZ,
562    ColorR,
563    ColorG,
564    ColorB,
565    ColorA,
566    Weight,
567    Custom,
568}
569
570impl ChannelType {
571    pub fn label(&self) -> &'static str {
572        match self {
573            Self::Generic   => "Value",
574            Self::PositionX => "X",
575            Self::PositionY => "Y",
576            Self::PositionZ => "Z",
577            Self::RotationX => "Rx",
578            Self::RotationY => "Ry",
579            Self::RotationZ => "Rz",
580            Self::ScaleX    => "Sx",
581            Self::ScaleY    => "Sy",
582            Self::ScaleZ    => "Sz",
583            Self::ColorR    => "R",
584            Self::ColorG    => "G",
585            Self::ColorB    => "B",
586            Self::ColorA    => "A",
587            Self::Weight    => "W",
588            Self::Custom    => "Custom",
589        }
590    }
591
592    pub fn color(&self) -> [f32; 3] {
593        match self {
594            Self::PositionX | Self::RotationX | Self::ScaleX | Self::ColorR
595                => [0.9, 0.3, 0.2],
596            Self::PositionY | Self::RotationY | Self::ScaleY | Self::ColorG
597                => [0.3, 0.9, 0.2],
598            Self::PositionZ | Self::RotationZ | Self::ScaleZ | Self::ColorB
599                => [0.2, 0.4, 0.9],
600            Self::ColorA | Self::Weight
601                => [0.7, 0.7, 0.7],
602            _   => [0.8, 0.8, 0.0],
603        }
604    }
605}
606
607#[derive(Debug, Clone)]
608pub struct CurveChannel {
609    pub name: String,
610    pub channel_type: ChannelType,
611    pub curve: CurveData,
612    pub enabled: bool,
613    pub locked: bool,
614    pub solo: bool,
615    pub min_value: f32,
616    pub max_value: f32,
617}
618
619impl CurveChannel {
620    pub fn new(name: String, ch: ChannelType) -> Self {
621        Self {
622            name,
623            channel_type: ch,
624            curve: CurveData::linear(),
625            enabled: true,
626            locked: false,
627            solo: false,
628            min_value: -1.0,
629            max_value: 1.0,
630        }
631    }
632
633    pub fn evaluate(&self, x: f32) -> f32 {
634        if !self.enabled { return 0.0; }
635        self.curve.evaluate(x)
636    }
637}
638
639// ─── Curve set (all channels for one animated property) ──────────────────────
640
641#[derive(Debug, Clone)]
642pub struct CurveSet {
643    pub name: String,
644    pub channels: Vec<CurveChannel>,
645    pub time_range: (f32, f32),
646    pub value_range: (f32, f32),
647}
648
649impl CurveSet {
650    pub fn new(name: String) -> Self {
651        Self {
652            name,
653            channels: Vec::new(),
654            time_range: (0.0, 1.0),
655            value_range: (-1.0, 1.0),
656        }
657    }
658
659    pub fn for_vec3(name: String) -> Self {
660        let mut cs = Self::new(name.clone());
661        cs.channels.push(CurveChannel::new(format!("{}.X", name), ChannelType::PositionX));
662        cs.channels.push(CurveChannel::new(format!("{}.Y", name), ChannelType::PositionY));
663        cs.channels.push(CurveChannel::new(format!("{}.Z", name), ChannelType::PositionZ));
664        cs
665    }
666
667    pub fn for_color(name: String) -> Self {
668        let mut cs = Self::new(name.clone());
669        cs.channels.push(CurveChannel::new(format!("{}.R", name), ChannelType::ColorR));
670        cs.channels.push(CurveChannel::new(format!("{}.G", name), ChannelType::ColorG));
671        cs.channels.push(CurveChannel::new(format!("{}.B", name), ChannelType::ColorB));
672        cs.channels.push(CurveChannel::new(format!("{}.A", name), ChannelType::ColorA));
673        cs
674    }
675
676    pub fn evaluate_at(&self, t: f32) -> Vec<f32> {
677        self.channels.iter().map(|c| c.evaluate(t)).collect()
678    }
679
680    pub fn add_channel(&mut self, ch: CurveChannel) {
681        self.channels.push(ch);
682    }
683
684    pub fn auto_fit_ranges(&mut self) {
685        let mut min_x = f32::MAX;
686        let mut max_x = f32::MIN;
687        let mut min_y = f32::MAX;
688        let mut max_y = f32::MIN;
689        for ch in &self.channels {
690            let (lo, hi) = ch.curve.bounds();
691            min_x = min_x.min(lo.x);
692            max_x = max_x.max(hi.x);
693            min_y = min_y.min(lo.y);
694            max_y = max_y.max(hi.y);
695        }
696        let pad_x = (max_x - min_x) * 0.05;
697        let pad_y = (max_y - min_y) * 0.1;
698        self.time_range  = (min_x - pad_x, max_x + pad_x);
699        self.value_range = (min_y - pad_y, max_y + pad_y);
700    }
701}
702
703// ─── Editor selection state ───────────────────────────────────────────────────
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq)]
706pub enum SelectionKind {
707    Point,
708    InHandle,
709    OutHandle,
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713pub struct PointSelection {
714    pub channel: usize,
715    pub point: usize,
716    pub kind: SelectionKind,
717}
718
719// ─── Editor undo ─────────────────────────────────────────────────────────────
720
721#[derive(Debug, Clone)]
722pub enum CurveEditAction {
723    MovePoint { channel: usize, point: usize, from: Vec2, to: Vec2 },
724    MoveInHandle { channel: usize, point: usize, from: Vec2, to: Vec2 },
725    MoveOutHandle { channel: usize, point: usize, from: Vec2, to: Vec2 },
726    AddPoint { channel: usize, pos: Vec2 },
727    RemovePoint { channel: usize, index: usize, data: ControlPoint },
728    SetCurveKind { channel: usize, from: CurveKind, to: CurveKind },
729    SetProceduralParam { channel: usize, param: String, from: f32, to: f32 },
730}
731
732impl fmt::Display for CurveEditAction {
733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
734        match self {
735            Self::MovePoint { .. } => write!(f, "Move Point"),
736            Self::AddPoint { .. }  => write!(f, "Add Point"),
737            Self::RemovePoint { .. } => write!(f, "Remove Point"),
738            Self::MoveInHandle { .. } => write!(f, "Move In Handle"),
739            Self::MoveOutHandle { .. } => write!(f, "Move Out Handle"),
740            Self::SetCurveKind { to, .. } => write!(f, "Set Curve Kind: {}", to.label()),
741            Self::SetProceduralParam { param, .. } => write!(f, "Set {}", param),
742        }
743    }
744}
745
746// ─── CurveEditor ─────────────────────────────────────────────────────────────
747
748#[derive(Debug)]
749pub struct CurveEditor {
750    pub curve_set: CurveSet,
751    pub visible_channels: Vec<bool>,
752    pub selected_points: Vec<PointSelection>,
753    pub canvas_time_range: (f32, f32),
754    pub canvas_value_range: (f32, f32),
755    pub canvas_size: Vec2,
756    pub grid_lines_x: u32,
757    pub grid_lines_y: u32,
758    pub show_tangent_handles: bool,
759    pub show_reference_line: bool,
760    pub reference_value: f32,
761    pub snap_x: bool,
762    pub snap_y: bool,
763    pub snap_x_step: f32,
764    pub snap_y_step: f32,
765    pub dragging: Option<PointSelection>,
766    pub drag_start_pos: Vec2,
767    undo_stack: Vec<CurveEditAction>,
768    redo_stack: Vec<CurveEditAction>,
769    pub frame_time: f32,
770    pub play_cursor: f32,
771    pub looping: bool,
772}
773
774impl CurveEditor {
775    pub fn new(canvas_w: f32, canvas_h: f32) -> Self {
776        let cs = CurveSet::new("Curve".into());
777        Self {
778            curve_set: cs,
779            visible_channels: Vec::new(),
780            selected_points: Vec::new(),
781            canvas_time_range: (0.0, 1.0),
782            canvas_value_range: (-1.0, 1.0),
783            canvas_size: Vec2::new(canvas_w, canvas_h),
784            grid_lines_x: 10,
785            grid_lines_y: 8,
786            show_tangent_handles: true,
787            show_reference_line: false,
788            reference_value: 0.0,
789            snap_x: false,
790            snap_y: false,
791            snap_x_step: 0.1,
792            snap_y_step: 0.1,
793            dragging: None,
794            drag_start_pos: Vec2::ZERO,
795            undo_stack: Vec::new(),
796            redo_stack: Vec::new(),
797            frame_time: 0.0,
798            play_cursor: 0.0,
799            looping: true,
800        }
801    }
802
803    pub fn load_set(&mut self, set: CurveSet) {
804        self.visible_channels = vec![true; set.channels.len()];
805        self.canvas_time_range  = set.time_range;
806        self.canvas_value_range = set.value_range;
807        self.curve_set = set;
808        self.selected_points.clear();
809    }
810
811    /// Canvas-space to curve-space
812    pub fn canvas_to_curve(&self, cx: f32, cy: f32) -> Vec2 {
813        let (t0, t1) = self.canvas_time_range;
814        let (v0, v1) = self.canvas_value_range;
815        Vec2::new(
816            t0 + (cx / self.canvas_size.x) * (t1 - t0),
817            v1 - (cy / self.canvas_size.y) * (v1 - v0),
818        )
819    }
820
821    /// Curve-space to canvas-space
822    pub fn curve_to_canvas(&self, tx: f32, ty: f32) -> Vec2 {
823        let (t0, t1) = self.canvas_time_range;
824        let (v0, v1) = self.canvas_value_range;
825        Vec2::new(
826            (tx - t0) / (t1 - t0) * self.canvas_size.x,
827            (v1 - ty) / (v1 - v0) * self.canvas_size.y,
828        )
829    }
830
831    pub fn add_point(&mut self, channel: usize, pos: Vec2) {
832        let pos = self.snapped(pos);
833        if let Some(ch) = self.curve_set.channels.get_mut(channel) {
834            ch.curve.add_point(pos);
835            self.undo_stack.push(CurveEditAction::AddPoint { channel, pos });
836            self.redo_stack.clear();
837        }
838    }
839
840    pub fn remove_point(&mut self, channel: usize, idx: usize) {
841        if let Some(ch) = self.curve_set.channels.get_mut(channel) {
842            let cp = ch.curve.points[idx];
843            ch.curve.remove_point(idx);
844            self.undo_stack.push(CurveEditAction::RemovePoint { channel, index: idx, data: cp });
845            self.redo_stack.clear();
846        }
847    }
848
849    pub fn move_point(&mut self, sel: PointSelection, new_pos: Vec2) {
850        let new_pos = self.snapped(new_pos);
851        if let Some(ch) = self.curve_set.channels.get_mut(sel.channel) {
852            if let Some(pt) = ch.curve.points.get_mut(sel.point) {
853                let from = pt.position;
854                match sel.kind {
855                    SelectionKind::Point => {
856                        let delta = new_pos - pt.position;
857                        pt.position = new_pos;
858                        pt.in_tangent  += delta;  // keep tangents relative
859                        pt.out_tangent += delta;
860                        self.undo_stack.push(CurveEditAction::MovePoint {
861                            channel: sel.channel, point: sel.point,
862                            from, to: new_pos,
863                        });
864                    }
865                    SelectionKind::InHandle => {
866                        let from_h = pt.in_tangent;
867                        pt.set_in_tangent_smooth(new_pos - pt.position);
868                        self.undo_stack.push(CurveEditAction::MoveInHandle {
869                            channel: sel.channel, point: sel.point,
870                            from: from_h, to: pt.in_tangent,
871                        });
872                    }
873                    SelectionKind::OutHandle => {
874                        let from_h = pt.out_tangent;
875                        pt.set_out_tangent_smooth(new_pos - pt.position);
876                        self.undo_stack.push(CurveEditAction::MoveOutHandle {
877                            channel: sel.channel, point: sel.point,
878                            from: from_h, to: pt.out_tangent,
879                        });
880                    }
881                }
882                self.redo_stack.clear();
883            }
884        }
885    }
886
887    fn snapped(&self, pos: Vec2) -> Vec2 {
888        Vec2::new(
889            if self.snap_x { (pos.x / self.snap_x_step).round() * self.snap_x_step } else { pos.x },
890            if self.snap_y { (pos.y / self.snap_y_step).round() * self.snap_y_step } else { pos.y },
891        )
892    }
893
894    pub fn set_kind(&mut self, channel: usize, kind: CurveKind) {
895        if let Some(ch) = self.curve_set.channels.get_mut(channel) {
896            let from = ch.curve.kind;
897            ch.curve.kind = kind;
898            self.undo_stack.push(CurveEditAction::SetCurveKind { channel, from, to: kind });
899            self.redo_stack.clear();
900        }
901    }
902
903    pub fn auto_smooth_tangents(&mut self, channel: usize) {
904        if let Some(ch) = self.curve_set.channels.get_mut(channel) {
905            let n = ch.curve.points.len();
906            for i in 0..n {
907                let prev = if i > 0 { ch.curve.points[i-1].position }
908                           else { ch.curve.points[i].position };
909                let next = if i+1 < n { ch.curve.points[i+1].position }
910                           else { ch.curve.points[i].position };
911                let tangent = (next - prev) * 0.3;
912                ch.curve.points[i].out_tangent = tangent;
913                ch.curve.points[i].in_tangent  = -tangent;
914                ch.curve.points[i].broken_tangents = false;
915            }
916        }
917    }
918
919    pub fn flatten_tangents(&mut self, channel: usize) {
920        if let Some(ch) = self.curve_set.channels.get_mut(channel) {
921            for pt in &mut ch.curve.points {
922                let out_len = pt.out_tangent.length();
923                let in_len  = pt.in_tangent.length();
924                pt.out_tangent = Vec2::new(out_len, 0.0);
925                pt.in_tangent  = Vec2::new(-in_len,  0.0);
926            }
927        }
928    }
929
930    pub fn select_all(&mut self) {
931        self.selected_points.clear();
932        for (ci, ch) in self.curve_set.channels.iter().enumerate() {
933            for pi in 0..ch.curve.points.len() {
934                self.selected_points.push(PointSelection {
935                    channel: ci, point: pi, kind: SelectionKind::Point,
936                });
937            }
938        }
939    }
940
941    pub fn deselect_all(&mut self) {
942        self.selected_points.clear();
943    }
944
945    pub fn undo(&mut self) {
946        if let Some(action) = self.undo_stack.pop() {
947            // Reverse the action
948            match &action {
949                CurveEditAction::MovePoint { channel, point, from, .. } => {
950                    let ch = *channel; let pt = *point; let f = *from;
951                    if let Some(c) = self.curve_set.channels.get_mut(ch) {
952                        if let Some(p) = c.curve.points.get_mut(pt) {
953                            p.position = f;
954                        }
955                    }
956                }
957                CurveEditAction::RemovePoint { channel, index, data } => {
958                    let ch = *channel; let i = *index; let d = *data;
959                    if let Some(c) = self.curve_set.channels.get_mut(ch) {
960                        c.curve.points.insert(i, d);
961                    }
962                }
963                CurveEditAction::AddPoint { channel, .. } => {
964                    let ch = *channel;
965                    if let Some(c) = self.curve_set.channels.get_mut(ch) {
966                        c.curve.points.pop();
967                    }
968                }
969                _ => {}
970            }
971            self.redo_stack.push(action);
972        }
973    }
974
975    pub fn redo(&mut self) {
976        if let Some(action) = self.redo_stack.pop() {
977            self.undo_stack.push(action);
978        }
979    }
980
981    pub fn advance_play_cursor(&mut self, dt: f32) {
982        let (t0, t1) = self.canvas_time_range;
983        self.play_cursor += dt;
984        if self.looping && self.play_cursor > t1 {
985            self.play_cursor = t0;
986        }
987        self.play_cursor = self.play_cursor.clamp(t0, t1);
988    }
989
990    pub fn evaluate_all_at_cursor(&self) -> Vec<f32> {
991        self.curve_set.evaluate_at(self.play_cursor)
992    }
993
994    pub fn frame_all(&mut self) {
995        self.curve_set.auto_fit_ranges();
996        let pad_t = (self.curve_set.time_range.1 - self.curve_set.time_range.0) * 0.05;
997        let pad_v = (self.curve_set.value_range.1 - self.curve_set.value_range.0) * 0.1;
998        self.canvas_time_range  = (
999            self.curve_set.time_range.0 - pad_t,
1000            self.curve_set.time_range.1 + pad_t,
1001        );
1002        self.canvas_value_range = (
1003            self.curve_set.value_range.0 - pad_v,
1004            self.curve_set.value_range.1 + pad_v,
1005        );
1006    }
1007
1008    pub fn zoom(&mut self, factor: f32, center_t: f32, center_v: f32) {
1009        let (t0, t1) = self.canvas_time_range;
1010        let (v0, v1) = self.canvas_value_range;
1011        let ht = (t1 - t0) * 0.5 / factor;
1012        let hv = (v1 - v0) * 0.5 / factor;
1013        self.canvas_time_range  = (center_t - ht, center_t + ht);
1014        self.canvas_value_range = (center_v - hv, center_v + hv);
1015    }
1016
1017    pub fn pan(&mut self, dt: f32, dv: f32) {
1018        self.canvas_time_range  = (self.canvas_time_range.0  + dt, self.canvas_time_range.1  + dt);
1019        self.canvas_value_range = (self.canvas_value_range.0 + dv, self.canvas_value_range.1 + dv);
1020    }
1021
1022    /// Build all polylines ready for GPU line drawing
1023    pub fn build_render_lines(&self) -> Vec<(usize, Vec<Vec2>)> {
1024        self.curve_set.channels.iter().enumerate()
1025            .filter(|(i, _)| self.visible_channels.get(*i).copied().unwrap_or(true))
1026            .map(|(i, ch)| {
1027                let pts = ch.curve.to_polyline(64);
1028                let canvas_pts = pts.iter().map(|p| self.curve_to_canvas(p.x, p.y)).collect();
1029                (i, canvas_pts)
1030            })
1031            .collect()
1032    }
1033}
1034
1035// ─── Tests ───────────────────────────────────────────────────────────────────
1036
1037#[cfg(test)]
1038mod tests {
1039    use super::*;
1040
1041    #[test]
1042    fn linear_interpolation() {
1043        let c = CurveData::linear();
1044        assert!((c.evaluate(0.0) - 0.0).abs() < 0.001);
1045        assert!((c.evaluate(1.0) - 1.0).abs() < 0.001);
1046        assert!((c.evaluate(0.5) - 0.5).abs() < 0.01);
1047    }
1048
1049    #[test]
1050    fn bezier_endpoints() {
1051        let c = CurveData::ease_in_out();
1052        assert!((c.evaluate(0.0) - 0.0).abs() < 0.01);
1053        assert!((c.evaluate(1.0) - 1.0).abs() < 0.01);
1054    }
1055
1056    #[test]
1057    fn sine_wave_zero_crossing() {
1058        let c = CurveData::new(CurveKind::SineWave);
1059        let v = c.evaluate(0.0);
1060        assert!(v.abs() < 0.1);
1061    }
1062
1063    #[test]
1064    fn constant_step() {
1065        let mut c = CurveData::new(CurveKind::Constant);
1066        c.points[0] = ControlPoint::new(Vec2::new(0.0, 0.0));
1067        c.points[1] = ControlPoint::new(Vec2::new(1.0, 1.0));
1068        assert!((c.evaluate(0.4) - 0.0).abs() < 0.001);
1069        assert!((c.evaluate(1.0) - 1.0).abs() < 0.001);
1070    }
1071
1072    #[test]
1073    fn canvas_curve_roundtrip() {
1074        let ed = CurveEditor::new(800.0, 400.0);
1075        let t = 0.6f32;
1076        let v = 0.3f32;
1077        let c = ed.curve_to_canvas(t, v);
1078        let back = ed.canvas_to_curve(c.x, c.y);
1079        assert!((back.x - t).abs() < 1e-4);
1080        assert!((back.y - v).abs() < 1e-4);
1081    }
1082
1083    #[test]
1084    fn add_remove_point() {
1085        let mut c = CurveData::linear();
1086        c.add_point(Vec2::new(0.5, 0.7));
1087        assert_eq!(c.points.len(), 3);
1088        c.remove_point(1);
1089        assert_eq!(c.points.len(), 2);
1090    }
1091
1092    #[test]
1093    fn spring_curve_settles_near_one() {
1094        let c = CurveData::new(CurveKind::Spring);
1095        let v = c.evaluate(10.0);
1096        // After a long time the damped oscillator should be near amplitude
1097        assert!((v - 1.0).abs() < 0.1, "spring value at t=10: {}", v);
1098    }
1099}