Skip to main content

proof_engine/editor/
modeling_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7// CONSTANTS
8// ============================================================
9
10pub const MAX_BONE_INFLUENCES: usize = 4;
11pub const MAX_LOD_LEVELS: usize = 4;
12pub const MARCHING_CUBES_THRESHOLD: f32 = 0.5;
13pub const METABALL_THRESHOLD: f32 = 1.0;
14pub const MAX_UNDO_STEPS: usize = 64;
15pub const DEFAULT_BRUSH_RADIUS: f32 = 1.0;
16pub const DEFAULT_BRUSH_STRENGTH: f32 = 0.5;
17pub const DEFAULT_BRUSH_DENSITY: f32 = 4.0;
18pub const EPSILON: f32 = 1e-6;
19pub const PI: f32 = std::f32::consts::PI;
20pub const TAU: f32 = std::f32::consts::TAU;
21pub const PHI: f32 = 1.618_033_9;
22
23// ============================================================
24// CORE DATA STRUCTURES
25// ============================================================
26
27#[derive(Clone, Debug)]
28pub struct Aabb3 {
29    pub min: Vec3,
30    pub max: Vec3,
31}
32
33impl Aabb3 {
34    pub fn new(min: Vec3, max: Vec3) -> Self {
35        Self { min, max }
36    }
37
38    pub fn empty() -> Self {
39        Self {
40            min: Vec3::splat(f32::MAX),
41            max: Vec3::splat(f32::MIN),
42        }
43    }
44
45    pub fn expand(&mut self, p: Vec3) {
46        self.min = self.min.min(p);
47        self.max = self.max.max(p);
48    }
49
50    pub fn center(&self) -> Vec3 {
51        (self.min + self.max) * 0.5
52    }
53
54    pub fn size(&self) -> Vec3 {
55        self.max - self.min
56    }
57
58    pub fn contains(&self, p: Vec3) -> bool {
59        p.x >= self.min.x && p.x <= self.max.x &&
60        p.y >= self.min.y && p.y <= self.max.y &&
61        p.z >= self.min.z && p.z <= self.max.z
62    }
63
64    pub fn intersects(&self, other: &Aabb3) -> bool {
65        self.min.x <= other.max.x && self.max.x >= other.min.x &&
66        self.min.y <= other.max.y && self.max.y >= other.min.y &&
67        self.min.z <= other.max.z && self.max.z >= other.min.z
68    }
69
70    pub fn surface_area(&self) -> f32 {
71        let s = self.size();
72        2.0 * (s.x * s.y + s.y * s.z + s.z * s.x)
73    }
74
75    pub fn volume(&self) -> f32 {
76        let s = self.size();
77        s.x * s.y * s.z
78    }
79}
80
81impl Default for Aabb3 {
82    fn default() -> Self {
83        Self::empty()
84    }
85}
86
87#[derive(Clone, Debug)]
88pub struct Ray3 {
89    pub origin: Vec3,
90    pub direction: Vec3,
91}
92
93impl Ray3 {
94    pub fn new(origin: Vec3, direction: Vec3) -> Self {
95        Self { origin, direction: direction.normalize() }
96    }
97
98    pub fn at(&self, t: f32) -> Vec3 {
99        self.origin + self.direction * t
100    }
101
102    pub fn distance_to_point(&self, p: Vec3) -> f32 {
103        let ap = p - self.origin;
104        let t = ap.dot(self.direction).max(0.0);
105        let closest = self.origin + self.direction * t;
106        (p - closest).length()
107    }
108
109    pub fn intersect_sphere(&self, center: Vec3, radius: f32) -> Option<f32> {
110        let oc = self.origin - center;
111        let a = self.direction.dot(self.direction);
112        let b = 2.0 * oc.dot(self.direction);
113        let c = oc.dot(oc) - radius * radius;
114        let discriminant = b * b - 4.0 * a * c;
115        if discriminant < 0.0 {
116            None
117        } else {
118            let t = (-b - discriminant.sqrt()) / (2.0 * a);
119            if t > EPSILON { Some(t) } else {
120                let t2 = (-b + discriminant.sqrt()) / (2.0 * a);
121                if t2 > EPSILON { Some(t2) } else { None }
122            }
123        }
124    }
125
126    pub fn intersect_aabb(&self, aabb: &Aabb3) -> Option<f32> {
127        let inv_dir = Vec3::new(
128            if self.direction.x.abs() > EPSILON { 1.0 / self.direction.x } else { f32::MAX },
129            if self.direction.y.abs() > EPSILON { 1.0 / self.direction.y } else { f32::MAX },
130            if self.direction.z.abs() > EPSILON { 1.0 / self.direction.z } else { f32::MAX },
131        );
132        let t1 = (aabb.min - self.origin) * inv_dir;
133        let t2 = (aabb.max - self.origin) * inv_dir;
134        let tmin = t1.min(t2);
135        let tmax = t1.max(t2);
136        let t_enter = tmin.x.max(tmin.y).max(tmin.z);
137        let t_exit  = tmax.x.min(tmax.y).min(tmax.z);
138        if t_enter <= t_exit && t_exit > 0.0 {
139            Some(if t_enter > 0.0 { t_enter } else { 0.0 })
140        } else {
141            None
142        }
143    }
144
145    pub fn intersect_plane(&self, plane_normal: Vec3, plane_d: f32) -> Option<f32> {
146        let denom = plane_normal.dot(self.direction);
147        if denom.abs() < EPSILON { return None; }
148        let t = (plane_d - plane_normal.dot(self.origin)) / denom;
149        if t > EPSILON { Some(t) } else { None }
150    }
151}
152
153// ============================================================
154// MODEL PARTICLE
155// ============================================================
156
157#[derive(Clone, Debug)]
158pub struct ModelParticle {
159    pub position:     Vec3,
160    pub character:    char,
161    pub color:        Vec4,
162    pub emission:     f32,
163    pub normal:       Vec3,
164    pub bone_weights: [f32; MAX_BONE_INFLUENCES],
165    pub bone_indices: [u8;  MAX_BONE_INFLUENCES],
166    pub group_id:     u32,
167    pub layer_id:     u8,
168    pub selected:     bool,
169    pub locked:       bool,
170}
171
172impl ModelParticle {
173    pub fn new(position: Vec3, character: char, color: Vec4) -> Self {
174        Self {
175            position,
176            character,
177            color,
178            emission:     0.0,
179            normal:       Vec3::Y,
180            bone_weights: [1.0, 0.0, 0.0, 0.0],
181            bone_indices: [0, 0, 0, 0],
182            group_id:     0,
183            layer_id:     0,
184            selected:     false,
185            locked:       false,
186        }
187    }
188
189    pub fn with_normal(mut self, normal: Vec3) -> Self {
190        self.normal = normal.normalize();
191        self
192    }
193
194    pub fn with_emission(mut self, emission: f32) -> Self {
195        self.emission = emission;
196        self
197    }
198
199    pub fn with_group(mut self, group_id: u32) -> Self {
200        self.group_id = group_id;
201        self
202    }
203
204    /// Returns the position snapped to a grid of given cell size.
205    pub fn snapped_position(&self, grid_size: f32) -> Vec3 {
206        if grid_size < EPSILON { return self.position; }
207        Vec3::new(
208            (self.position.x / grid_size).round() * grid_size,
209            (self.position.y / grid_size).round() * grid_size,
210            (self.position.z / grid_size).round() * grid_size,
211        )
212    }
213}
214
215impl Default for ModelParticle {
216    fn default() -> Self {
217        Self::new(Vec3::ZERO, '.', Vec4::ONE)
218    }
219}
220
221// ============================================================
222// SKELETON / BONES
223// ============================================================
224
225#[derive(Clone, Debug)]
226pub struct ParticleBone {
227    pub id:        u32,
228    pub name:      String,
229    pub head:      Vec3,
230    pub tail:      Vec3,
231    pub parent_id: Option<u32>,
232    pub rest_matrix:  Mat4,
233    pub pose_matrix:  Mat4,
234    pub local_rotation: Quat,
235    pub local_scale:    Vec3,
236}
237
238impl ParticleBone {
239    pub fn new(id: u32, name: impl Into<String>, head: Vec3, tail: Vec3) -> Self {
240        let rest_matrix = Mat4::from_translation(head);
241        Self {
242            id,
243            name: name.into(),
244            head,
245            tail,
246            parent_id: None,
247            rest_matrix,
248            pose_matrix: rest_matrix,
249            local_rotation: Quat::IDENTITY,
250            local_scale: Vec3::ONE,
251        }
252    }
253
254    pub fn length(&self) -> f32 {
255        (self.tail - self.head).length()
256    }
257
258    pub fn direction(&self) -> Vec3 {
259        (self.tail - self.head).normalize()
260    }
261
262    pub fn closest_point_on_bone(&self, p: Vec3) -> Vec3 {
263        let dir = self.tail - self.head;
264        let len = dir.length();
265        if len < EPSILON { return self.head; }
266        let t = ((p - self.head).dot(dir) / (len * len)).clamp(0.0, 1.0);
267        self.head + dir * t
268    }
269
270    pub fn distance_to_point(&self, p: Vec3) -> f32 {
271        (p - self.closest_point_on_bone(p)).length()
272    }
273
274    pub fn build_pose_matrix(&self) -> Mat4 {
275        Mat4::from_scale_rotation_translation(
276            self.local_scale,
277            self.local_rotation,
278            self.head,
279        )
280    }
281}
282
283#[derive(Clone, Debug)]
284pub struct ParticleSkeleton {
285    pub bones:      Vec<ParticleBone>,
286    pub bind_poses: Vec<Mat4>,
287}
288
289impl ParticleSkeleton {
290    pub fn new() -> Self {
291        Self { bones: Vec::new(), bind_poses: Vec::new() }
292    }
293
294    pub fn add_bone(&mut self, bone: ParticleBone) -> u32 {
295        let id = bone.id;
296        self.bind_poses.push(bone.rest_matrix);
297        self.bones.push(bone);
298        id
299    }
300
301    pub fn find_bone(&self, id: u32) -> Option<&ParticleBone> {
302        self.bones.iter().find(|b| b.id == id)
303    }
304
305    pub fn find_bone_mut(&mut self, id: u32) -> Option<&mut ParticleBone> {
306        self.bones.iter_mut().find(|b| b.id == id)
307    }
308
309    /// Compute skinning weights for a particle position.
310    /// Returns arrays of (bone_index, weight) for N nearest bones.
311    pub fn compute_skin_weights(&self, position: Vec3, num_influences: usize) -> (Vec<usize>, Vec<f32>) {
312        let mut dist_pairs: Vec<(usize, f32)> = self.bones.iter().enumerate()
313            .map(|(i, b)| (i, b.distance_to_point(position)))
314            .collect();
315        dist_pairs.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
316        dist_pairs.truncate(num_influences);
317
318        let mut indices = Vec::new();
319        let mut weights = Vec::new();
320        let mut total_weight = 0.0f32;
321
322        for (idx, dist) in &dist_pairs {
323            let w = if *dist < EPSILON { 1e6 } else { 1.0 / (dist * dist) };
324            indices.push(*idx);
325            weights.push(w);
326            total_weight += w;
327        }
328
329        if total_weight > EPSILON {
330            for w in &mut weights { *w /= total_weight; }
331        }
332
333        (indices, weights)
334    }
335
336    /// Apply pose transforms to a position.
337    pub fn transform_position(&self, position: Vec3, bone_indices: &[u8; 4], bone_weights: &[f32; 4]) -> Vec3 {
338        let mut result = Vec3::ZERO;
339        for i in 0..MAX_BONE_INFLUENCES {
340            let w = bone_weights[i];
341            if w < EPSILON { continue; }
342            let bi = bone_indices[i] as usize;
343            if bi >= self.bones.len() { continue; }
344            let pose  = self.bones[bi].pose_matrix;
345            let bind_inv = self.bind_poses[bi].inverse();
346            let skinned = pose * bind_inv * Vec4::new(position.x, position.y, position.z, 1.0);
347            result += Vec3::new(skinned.x, skinned.y, skinned.z) * w;
348        }
349        result
350    }
351
352    pub fn bind_all_particles(&mut self, particles: &mut Vec<ModelParticle>) {
353        for p in particles.iter_mut() {
354            let (indices, weights) = self.compute_skin_weights(p.position, MAX_BONE_INFLUENCES);
355            for i in 0..MAX_BONE_INFLUENCES {
356                p.bone_indices[i] = indices.get(i).copied().unwrap_or(0) as u8;
357                p.bone_weights[i] = weights.get(i).copied().unwrap_or(0.0);
358            }
359        }
360    }
361}
362
363impl Default for ParticleSkeleton {
364    fn default() -> Self { Self::new() }
365}
366
367// ============================================================
368// LOD
369// ============================================================
370
371#[derive(Clone, Debug)]
372pub struct LodLevel {
373    pub level:       u8,
374    pub particles:   Vec<usize>,  // indices into parent model's particles
375    pub distance:    f32,
376    pub density_pct: f32,
377}
378
379impl LodLevel {
380    pub fn new(level: u8, distance: f32, density_pct: f32) -> Self {
381        Self { level, particles: Vec::new(), distance, density_pct }
382    }
383}
384
385// ============================================================
386// LAYER
387// ============================================================
388
389#[derive(Clone, Debug)]
390pub enum LayerBlendMode {
391    Replace,
392    Add,
393    Mask,
394}
395
396#[derive(Clone, Debug)]
397pub struct ModelLayer {
398    pub id:         u8,
399    pub name:       String,
400    pub visible:    bool,
401    pub locked:     bool,
402    pub opacity:    f32,
403    pub blend_mode: LayerBlendMode,
404    pub particle_indices: Vec<usize>,
405}
406
407impl ModelLayer {
408    pub fn new(id: u8, name: impl Into<String>) -> Self {
409        Self {
410            id,
411            name: name.into(),
412            visible:    true,
413            locked:     false,
414            opacity:    1.0,
415            blend_mode: LayerBlendMode::Replace,
416            particle_indices: Vec::new(),
417        }
418    }
419
420    pub fn toggle_visibility(&mut self) { self.visible = !self.visible; }
421    pub fn toggle_lock(&mut self) { self.locked = !self.locked; }
422}
423
424// ============================================================
425// PARTICLE MODEL
426// ============================================================
427
428#[derive(Clone, Debug)]
429pub struct ParticleModel {
430    pub id:         u64,
431    pub name:       String,
432    pub particles:  Vec<ModelParticle>,
433    pub bounds:     Aabb3,
434    pub lod_levels: Vec<LodLevel>,
435    pub skeleton:   Option<ParticleSkeleton>,
436    pub layers:     Vec<ModelLayer>,
437    pub metadata:   HashMap<String, String>,
438}
439
440impl ParticleModel {
441    pub fn new(id: u64, name: impl Into<String>) -> Self {
442        let mut model = Self {
443            id,
444            name: name.into(),
445            particles:  Vec::new(),
446            bounds:     Aabb3::empty(),
447            lod_levels: Vec::new(),
448            skeleton:   None,
449            layers:     Vec::new(),
450            metadata:   HashMap::new(),
451        };
452        model.layers.push(ModelLayer::new(0, "Layer 0"));
453        model
454    }
455
456    pub fn recompute_bounds(&mut self) {
457        self.bounds = Aabb3::empty();
458        for p in &self.particles {
459            self.bounds.expand(p.position);
460        }
461    }
462
463    pub fn center_of_mass(&self) -> Vec3 {
464        if self.particles.is_empty() { return Vec3::ZERO; }
465        let sum: Vec3 = self.particles.iter().map(|p| p.position).fold(Vec3::ZERO, |a, b| a + b);
466        sum / self.particles.len() as f32
467    }
468
469    pub fn add_particle(&mut self, p: ModelParticle) -> usize {
470        let idx = self.particles.len();
471        self.bounds.expand(p.position);
472        if let Some(layer) = self.layers.last_mut() {
473            layer.particle_indices.push(idx);
474        }
475        self.particles.push(p);
476        idx
477    }
478
479    pub fn add_particles_bulk(&mut self, new_particles: Vec<ModelParticle>) {
480        let start = self.particles.len();
481        for (i, p) in new_particles.into_iter().enumerate() {
482            self.bounds.expand(p.position);
483            if let Some(layer) = self.layers.last_mut() {
484                layer.particle_indices.push(start + i);
485            }
486            self.particles.push(p);
487        }
488    }
489
490    pub fn remove_particles(&mut self, indices: &HashSet<usize>) {
491        let mut new_particles = Vec::with_capacity(self.particles.len());
492        let mut remap: Vec<Option<usize>> = vec![None; self.particles.len()];
493        let mut new_idx = 0;
494        for (old_idx, p) in self.particles.drain(..).enumerate() {
495            if !indices.contains(&old_idx) {
496                remap[old_idx] = Some(new_idx);
497                new_particles.push(p);
498                new_idx += 1;
499            }
500        }
501        self.particles = new_particles;
502        // Remap layer indices
503        for layer in &mut self.layers {
504            layer.particle_indices = layer.particle_indices.iter()
505                .filter_map(|&i| remap.get(i).copied().flatten())
506                .collect();
507        }
508        self.recompute_bounds();
509    }
510
511    pub fn particles_in_radius(&self, center: Vec3, radius: f32) -> Vec<usize> {
512        let r2 = radius * radius;
513        self.particles.iter().enumerate()
514            .filter(|(_, p)| (p.position - center).length_squared() <= r2)
515            .map(|(i, _)| i)
516            .collect()
517    }
518
519    pub fn particles_in_aabb(&self, aabb: &Aabb3) -> Vec<usize> {
520        self.particles.iter().enumerate()
521            .filter(|(_, p)| aabb.contains(p.position))
522            .map(|(i, _)| i)
523            .collect()
524    }
525
526    pub fn generate_lods(&mut self) {
527        self.lod_levels.clear();
528        let total = self.particles.len();
529        let configs: [(u8, f32, f32); 4] = [
530            (0, 10.0,  1.00),
531            (1, 30.0,  0.50),
532            (2, 60.0,  0.25),
533            (3, 120.0, 0.10),
534        ];
535        for (level, distance, pct) in &configs {
536            let mut lod = LodLevel::new(*level, *distance, *pct);
537            let target = ((total as f32) * pct).round() as usize;
538            lod.particles = subsample_indices(total, target);
539            self.lod_levels.push(lod);
540        }
541    }
542
543    pub fn select_lod(&self, camera_distance: f32, lod_bias: f32) -> usize {
544        let adjusted = camera_distance * (1.0 + lod_bias);
545        for (i, lod) in self.lod_levels.iter().enumerate().rev() {
546            if adjusted >= lod.distance {
547                return i;
548            }
549        }
550        0
551    }
552
553    pub fn merge_layer_into(&mut self, src_id: u8, dst_id: u8) {
554        let src_idx = self.layers.iter().position(|l| l.id == src_id);
555        let dst_idx = self.layers.iter().position(|l| l.id == dst_id);
556        if let (Some(si), Some(di)) = (src_idx, dst_idx) {
557            let src_indices = self.layers[si].particle_indices.clone();
558            for idx in src_indices {
559                self.layers[di].particle_indices.push(idx);
560            }
561            // Update layer_id on particles
562            for &pi in &self.layers[di].particle_indices {
563                if let Some(p) = self.particles.get_mut(pi) {
564                    p.layer_id = dst_id;
565                }
566            }
567            self.layers.remove(si);
568        }
569    }
570
571    pub fn add_layer(&mut self, name: impl Into<String>) -> u8 {
572        let id = self.layers.len() as u8;
573        self.layers.push(ModelLayer::new(id, name));
574        id
575    }
576}
577
578// Helper: subsample indices evenly
579fn subsample_indices(total: usize, target: usize) -> Vec<usize> {
580    if target >= total { return (0..total).collect(); }
581    if target == 0 { return Vec::new(); }
582    let step = total as f32 / target as f32;
583    (0..target).map(|i| ((i as f32 * step) as usize).min(total - 1)).collect()
584}
585
586// ============================================================
587// BRUSH TYPES
588// ============================================================
589
590#[derive(Clone, Debug, PartialEq)]
591pub enum BrushKind {
592    Add,
593    Remove,
594    Smooth,
595    Inflate,
596    Pinch,
597    Color,
598    Char,
599    Flatten,
600    Crease,
601    Clone,
602}
603
604impl Default for BrushKind {
605    fn default() -> Self { BrushKind::Add }
606}
607
608#[derive(Clone, Debug)]
609pub struct BrushParams {
610    pub kind:     BrushKind,
611    pub radius:   f32,
612    pub strength: f32,
613    pub density:  f32,
614    pub color:    Vec4,
615    pub character: char,
616    pub falloff:  FalloffCurve,
617}
618
619impl Default for BrushParams {
620    fn default() -> Self {
621        Self {
622            kind:      BrushKind::Add,
623            radius:    DEFAULT_BRUSH_RADIUS,
624            strength:  DEFAULT_BRUSH_STRENGTH,
625            density:   DEFAULT_BRUSH_DENSITY,
626            color:     Vec4::ONE,
627            character: '.',
628            falloff:   FalloffCurve::Smooth,
629        }
630    }
631}
632
633// ============================================================
634// FALLOFF CURVES
635// ============================================================
636
637#[derive(Clone, Debug, PartialEq)]
638pub enum FalloffCurve {
639    Constant,
640    Linear,
641    Smooth,
642    Sphere,
643    Root,
644    Sharp,
645}
646
647impl FalloffCurve {
648    /// Returns weight in [0,1] given normalized distance t in [0,1].
649    pub fn evaluate(&self, t: f32) -> f32 {
650        let t = t.clamp(0.0, 1.0);
651        match self {
652            FalloffCurve::Constant => 1.0,
653            FalloffCurve::Linear   => 1.0 - t,
654            FalloffCurve::Smooth   => smoothstep(0.0, 1.0, 1.0 - t),
655            FalloffCurve::Sphere   => (1.0 - t * t).max(0.0).sqrt(),
656            FalloffCurve::Root     => (1.0 - t).sqrt(),
657            FalloffCurve::Sharp    => (1.0 - t).powi(3),
658        }
659    }
660}
661
662#[inline]
663pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
664    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
665    t * t * (3.0 - 2.0 * t)
666}
667
668#[inline]
669pub fn smootherstep(edge0: f32, edge1: f32, x: f32) -> f32 {
670    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
671    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
672}
673
674// ============================================================
675// SYMMETRY
676// ============================================================
677
678#[derive(Clone, Debug, PartialEq)]
679pub enum SymmetryMode {
680    None,
681    X, Y, Z,
682    XY, XZ, YZ,
683    XYZ,
684}
685
686impl SymmetryMode {
687    /// Returns mirror positions for a given input position.
688    pub fn mirrors(&self, p: Vec3) -> Vec<Vec3> {
689        match self {
690            SymmetryMode::None => vec![],
691            SymmetryMode::X    => vec![Vec3::new(-p.x,  p.y,  p.z)],
692            SymmetryMode::Y    => vec![Vec3::new( p.x, -p.y,  p.z)],
693            SymmetryMode::Z    => vec![Vec3::new( p.x,  p.y, -p.z)],
694            SymmetryMode::XY   => vec![
695                Vec3::new(-p.x,  p.y,  p.z),
696                Vec3::new( p.x, -p.y,  p.z),
697                Vec3::new(-p.x, -p.y,  p.z),
698            ],
699            SymmetryMode::XZ   => vec![
700                Vec3::new(-p.x,  p.y,  p.z),
701                Vec3::new( p.x,  p.y, -p.z),
702                Vec3::new(-p.x,  p.y, -p.z),
703            ],
704            SymmetryMode::YZ   => vec![
705                Vec3::new( p.x, -p.y,  p.z),
706                Vec3::new( p.x,  p.y, -p.z),
707                Vec3::new( p.x, -p.y, -p.z),
708            ],
709            SymmetryMode::XYZ  => vec![
710                Vec3::new(-p.x,  p.y,  p.z),
711                Vec3::new( p.x, -p.y,  p.z),
712                Vec3::new( p.x,  p.y, -p.z),
713                Vec3::new(-p.x, -p.y,  p.z),
714                Vec3::new(-p.x,  p.y, -p.z),
715                Vec3::new( p.x, -p.y, -p.z),
716                Vec3::new(-p.x, -p.y, -p.z),
717            ],
718        }
719    }
720}
721
722// ============================================================
723// SELECTION SYSTEM
724// ============================================================
725
726#[derive(Clone, Debug, Default)]
727pub struct SelectionSystem {
728    pub selected: HashSet<usize>,
729    pub named_sets: HashMap<String, HashSet<usize>>,
730}
731
732impl SelectionSystem {
733    pub fn new() -> Self { Self::default() }
734
735    pub fn clear(&mut self) {
736        self.selected.clear();
737    }
738
739    pub fn select_all(&mut self, count: usize) {
740        self.selected = (0..count).collect();
741    }
742
743    pub fn invert(&mut self, total: usize) {
744        let all: HashSet<usize> = (0..total).collect();
745        self.selected = all.difference(&self.selected).copied().collect();
746    }
747
748    pub fn add(&mut self, idx: usize) { self.selected.insert(idx); }
749    pub fn remove(&mut self, idx: usize) { self.selected.remove(&idx); }
750    pub fn toggle(&mut self, idx: usize) {
751        if self.selected.contains(&idx) { self.selected.remove(&idx); }
752        else { self.selected.insert(idx); }
753    }
754
755    pub fn box_select(&mut self, particles: &[ModelParticle], aabb: &Aabb3, add: bool) {
756        if !add { self.selected.clear(); }
757        for (i, p) in particles.iter().enumerate() {
758            if aabb.contains(p.position) { self.selected.insert(i); }
759        }
760    }
761
762    pub fn sphere_select(&mut self, particles: &[ModelParticle], center: Vec3, radius: f32, add: bool) {
763        if !add { self.selected.clear(); }
764        let r2 = radius * radius;
765        for (i, p) in particles.iter().enumerate() {
766            if (p.position - center).length_squared() <= r2 {
767                self.selected.insert(i);
768            }
769        }
770    }
771
772    pub fn paint_select(&mut self, particles: &[ModelParticle], ray: &Ray3, radius: f32, add: bool) {
773        if !add { self.selected.clear(); }
774        for (i, p) in particles.iter().enumerate() {
775            if ray.distance_to_point(p.position) <= radius {
776                self.selected.insert(i);
777            }
778        }
779    }
780
781    pub fn select_by_group(&mut self, particles: &[ModelParticle], group_id: u32, add: bool) {
782        if !add { self.selected.clear(); }
783        for (i, p) in particles.iter().enumerate() {
784            if p.group_id == group_id { self.selected.insert(i); }
785        }
786    }
787
788    pub fn select_by_char(&mut self, particles: &[ModelParticle], ch: char, add: bool) {
789        if !add { self.selected.clear(); }
790        for (i, p) in particles.iter().enumerate() {
791            if p.character == ch { self.selected.insert(i); }
792        }
793    }
794
795    /// HSV distance based color selection.
796    pub fn select_by_color_range(
797        &mut self,
798        particles: &[ModelParticle],
799        target_color: Vec4,
800        tolerance: f32,
801        add: bool,
802    ) {
803        if !add { self.selected.clear(); }
804        let th = rgb_to_hsv(target_color.x, target_color.y, target_color.z);
805        for (i, p) in particles.iter().enumerate() {
806            let ph = rgb_to_hsv(p.color.x, p.color.y, p.color.z);
807            let dh = hue_distance(th.0, ph.0);
808            let ds = (th.1 - ph.1).abs();
809            let dv = (th.2 - ph.2).abs();
810            let dist = (dh * dh + ds * ds + dv * dv).sqrt();
811            if dist <= tolerance { self.selected.insert(i); }
812        }
813    }
814
815    /// Grow selection: add particles adjacent (within radius) to current selection.
816    pub fn grow(&mut self, particles: &[ModelParticle], radius: f32) {
817        let current: Vec<usize> = self.selected.iter().copied().collect();
818        let r2 = radius * radius;
819        for (i, p) in particles.iter().enumerate() {
820            if self.selected.contains(&i) { continue; }
821            for &sel in &current {
822                if (particles[sel].position - p.position).length_squared() <= r2 {
823                    self.selected.insert(i);
824                    break;
825                }
826            }
827        }
828    }
829
830    /// Shrink selection: remove boundary particles (those with non-selected neighbors).
831    pub fn shrink(&mut self, particles: &[ModelParticle], radius: f32) {
832        let r2 = radius * radius;
833        let to_remove: HashSet<usize> = self.selected.iter().copied().filter(|&si| {
834            particles.iter().enumerate().any(|(i, p)| {
835                !self.selected.contains(&i) &&
836                (particles[si].position - p.position).length_squared() <= r2
837            })
838        }).collect();
839        for idx in to_remove { self.selected.remove(&idx); }
840    }
841
842    pub fn save_named_set(&mut self, name: impl Into<String>) {
843        self.named_sets.insert(name.into(), self.selected.clone());
844    }
845
846    pub fn load_named_set(&mut self, name: &str) {
847        if let Some(set) = self.named_sets.get(name) {
848            self.selected = set.clone();
849        }
850    }
851
852    pub fn union_named_set(&mut self, name: &str) {
853        if let Some(set) = self.named_sets.get(name) {
854            for &idx in set { self.selected.insert(idx); }
855        }
856    }
857}
858
859// ============================================================
860// COLOR UTILITIES
861// ============================================================
862
863pub fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
864    let max = r.max(g).max(b);
865    let min = r.min(g).min(b);
866    let delta = max - min;
867    let v = max;
868    let s = if max > EPSILON { delta / max } else { 0.0 };
869    let h = if delta < EPSILON {
870        0.0
871    } else if (max - r).abs() < EPSILON {
872        60.0 * (((g - b) / delta) % 6.0)
873    } else if (max - g).abs() < EPSILON {
874        60.0 * ((b - r) / delta + 2.0)
875    } else {
876        60.0 * ((r - g) / delta + 4.0)
877    };
878    let h = if h < 0.0 { h + 360.0 } else { h };
879    (h, s, v)
880}
881
882pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
883    if s < EPSILON { return (v, v, v); }
884    let h = h % 360.0;
885    let c = v * s;
886    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
887    let m = v - c;
888    let (r1, g1, b1) = match (h / 60.0) as u32 {
889        0 => (c, x, 0.0),
890        1 => (x, c, 0.0),
891        2 => (0.0, c, x),
892        3 => (0.0, x, c),
893        4 => (x, 0.0, c),
894        _ => (c, 0.0, x),
895    };
896    (r1 + m, g1 + m, b1 + m)
897}
898
899pub fn hue_distance(a: f32, b: f32) -> f32 {
900    let d = (a - b).abs() % 360.0;
901    if d > 180.0 { (360.0 - d) / 180.0 } else { d / 180.0 }
902}
903
904// ============================================================
905// PRIMITIVE GENERATORS
906// ============================================================
907
908pub struct PrimitiveBuilder;
909
910impl PrimitiveBuilder {
911    /// Fibonacci sphere — uniform distribution of N points on a sphere surface.
912    pub fn sphere(center: Vec3, radius: f32, n: usize, character: char, color: Vec4) -> Vec<ModelParticle> {
913        let mut particles = Vec::with_capacity(n);
914        let golden_angle = PI * (3.0 - 5.0_f32.sqrt());
915        for i in 0..n {
916            let y = 1.0 - (i as f32 / (n as f32 - 1.0)) * 2.0;
917            let r = (1.0 - y * y).max(0.0).sqrt();
918            let theta = golden_angle * i as f32;
919            let x = theta.cos() * r;
920            let z = theta.sin() * r;
921            let normal = Vec3::new(x, y, z).normalize();
922            let pos = center + normal * radius;
923            particles.push(ModelParticle::new(pos, character, color).with_normal(normal));
924        }
925        particles
926    }
927
928    /// Cube — particles on 6 faces, with optional interior fill.
929    pub fn cube(
930        center: Vec3,
931        half_size: Vec3,
932        particles_per_face: usize,
933        fill_interior: bool,
934        character: char,
935        color: Vec4,
936    ) -> Vec<ModelParticle> {
937        let mut particles = Vec::new();
938        let n = (particles_per_face as f32).sqrt().ceil() as usize;
939        let faces: [(Vec3, Vec3, Vec3); 6] = [
940            (Vec3::X,  Vec3::Y,  Vec3::Z),
941            (-Vec3::X, Vec3::Z,  Vec3::Y),
942            (Vec3::Y,  Vec3::X,  Vec3::Z),
943            (-Vec3::Y, Vec3::Z,  Vec3::X),
944            (Vec3::Z,  Vec3::X,  Vec3::Y),
945            (-Vec3::Z, Vec3::Y,  Vec3::X),
946        ];
947        for (normal, u_axis, v_axis) in &faces {
948            let face_center = center + *normal * (normal.abs().dot(half_size));
949            let hu = u_axis.abs().dot(half_size);
950            let hv = v_axis.abs().dot(half_size);
951            for ui in 0..n {
952                for vi in 0..n {
953                    let u = (ui as f32 / (n as f32 - 1.0).max(1.0)) * 2.0 - 1.0;
954                    let v = (vi as f32 / (n as f32 - 1.0).max(1.0)) * 2.0 - 1.0;
955                    let pos = face_center + *u_axis * (u * hu) + *v_axis * (v * hv);
956                    particles.push(ModelParticle::new(pos, character, color).with_normal(*normal));
957                }
958            }
959        }
960        if fill_interior {
961            let steps = (particles_per_face as f32).cbrt().ceil() as usize;
962            for xi in 0..steps {
963                for yi in 0..steps {
964                    for zi in 0..steps {
965                        let x = (xi as f32 / steps as f32) * 2.0 - 1.0;
966                        let y = (yi as f32 / steps as f32) * 2.0 - 1.0;
967                        let z = (zi as f32 / steps as f32) * 2.0 - 1.0;
968                        let pos = center + Vec3::new(x * half_size.x, y * half_size.y, z * half_size.z);
969                        particles.push(ModelParticle::new(pos, character, color).with_normal(Vec3::Y));
970                    }
971                }
972            }
973        }
974        particles
975    }
976
977    /// Cylinder — circular bands + caps.
978    pub fn cylinder(
979        center: Vec3,
980        radius: f32,
981        height: f32,
982        segments: usize,
983        bands: usize,
984        character: char,
985        color: Vec4,
986    ) -> Vec<ModelParticle> {
987        let mut particles = Vec::new();
988        let half_h = height * 0.5;
989        // Side bands
990        for bi in 0..bands {
991            let y = (bi as f32 / (bands as f32 - 1.0).max(1.0)) * height - half_h + center.y;
992            for si in 0..segments {
993                let angle = TAU * si as f32 / segments as f32;
994                let x = center.x + radius * angle.cos();
995                let z = center.z + radius * angle.sin();
996                let normal = Vec3::new(angle.cos(), 0.0, angle.sin());
997                particles.push(ModelParticle::new(Vec3::new(x, y, z), character, color).with_normal(normal));
998            }
999        }
1000        // Caps
1001        for cap in [half_h, -half_h] {
1002            let normal = if cap > 0.0 { Vec3::Y } else { -Vec3::Y };
1003            let n = (segments as f32).sqrt().ceil() as usize;
1004            for ri in 0..n {
1005                let r = (ri as f32 / n as f32) * radius;
1006                for si in 0..segments {
1007                    let angle = TAU * si as f32 / segments as f32;
1008                    let x = center.x + r * angle.cos();
1009                    let z = center.z + r * angle.sin();
1010                    let y = center.y + cap;
1011                    particles.push(ModelParticle::new(Vec3::new(x, y, z), character, color).with_normal(normal));
1012                }
1013            }
1014        }
1015        particles
1016    }
1017
1018    /// Cone.
1019    pub fn cone(
1020        apex: Vec3,
1021        base_center: Vec3,
1022        base_radius: f32,
1023        segments: usize,
1024        bands: usize,
1025        character: char,
1026        color: Vec4,
1027    ) -> Vec<ModelParticle> {
1028        let mut particles = Vec::new();
1029        let axis = (apex - base_center).normalize();
1030        let height = (apex - base_center).length();
1031        let perp = if axis.abs().dot(Vec3::X) < 0.9 {
1032            axis.cross(Vec3::X).normalize()
1033        } else {
1034            axis.cross(Vec3::Y).normalize()
1035        };
1036        let perp2 = axis.cross(perp).normalize();
1037        for bi in 0..bands {
1038            let t = bi as f32 / bands as f32;
1039            let r = base_radius * (1.0 - t);
1040            let pos_center = base_center + axis * (t * height);
1041            for si in 0..segments {
1042                let angle = TAU * si as f32 / segments as f32;
1043                let pos = pos_center + perp * (angle.cos() * r) + perp2 * (angle.sin() * r);
1044                let outward = (perp * angle.cos() + perp2 * angle.sin()).normalize();
1045                let slope = (base_radius / height).atan();
1046                let normal = (outward + axis * slope.tan()).normalize();
1047                particles.push(ModelParticle::new(pos, character, color).with_normal(normal));
1048            }
1049        }
1050        // Base cap
1051        let n = (segments as f32 / 2.0).ceil() as usize;
1052        for ri in 0..=n {
1053            let r = (ri as f32 / n as f32) * base_radius;
1054            for si in 0..segments {
1055                let angle = TAU * si as f32 / segments as f32;
1056                let pos = base_center + perp * (angle.cos() * r) + perp2 * (angle.sin() * r);
1057                particles.push(ModelParticle::new(pos, character, color).with_normal(-axis));
1058            }
1059        }
1060        particles
1061    }
1062
1063    /// Torus — parametric surface: (R + r*cos(v)) * cos(u), etc.
1064    pub fn torus(
1065        center: Vec3,
1066        major_radius: f32,
1067        minor_radius: f32,
1068        u_segments: usize,
1069        v_segments: usize,
1070        character: char,
1071        color: Vec4,
1072    ) -> Vec<ModelParticle> {
1073        let mut particles = Vec::new();
1074        for ui in 0..u_segments {
1075            let u = TAU * ui as f32 / u_segments as f32;
1076            for vi in 0..v_segments {
1077                let v = TAU * vi as f32 / v_segments as f32;
1078                let x = (major_radius + minor_radius * v.cos()) * u.cos();
1079                let y = minor_radius * v.sin();
1080                let z = (major_radius + minor_radius * v.cos()) * u.sin();
1081                // Normal: derivative of surface position w.r.t. v, cross u, simplified to outward normal
1082                let ring_center = Vec3::new(major_radius * u.cos(), 0.0, major_radius * u.sin());
1083                let surface_pos = center + Vec3::new(x, y, z);
1084                let normal = (surface_pos - (center + ring_center)).normalize();
1085                particles.push(ModelParticle::new(surface_pos, character, color).with_normal(normal));
1086            }
1087        }
1088        particles
1089    }
1090
1091    /// Plane — grid of particles with optional Perlin-like noise displacement.
1092    pub fn plane(
1093        center: Vec3,
1094        width: f32,
1095        depth: f32,
1096        cols: usize,
1097        rows: usize,
1098        noise_scale: f32,
1099        noise_amplitude: f32,
1100        character: char,
1101        color: Vec4,
1102    ) -> Vec<ModelParticle> {
1103        let mut particles = Vec::new();
1104        let hw = width * 0.5;
1105        let hd = depth * 0.5;
1106        for ri in 0..rows {
1107            for ci in 0..cols {
1108                let u = ci as f32 / (cols as f32 - 1.0).max(1.0);
1109                let v = ri as f32 / (rows as f32 - 1.0).max(1.0);
1110                let x = center.x - hw + u * width;
1111                let z = center.z - hd + v * depth;
1112                let noise = simple_noise_2d(x * noise_scale, z * noise_scale);
1113                let y = center.y + noise * noise_amplitude;
1114                particles.push(ModelParticle::new(Vec3::new(x, y, z), character, color).with_normal(Vec3::Y));
1115            }
1116        }
1117        particles
1118    }
1119
1120    /// Text3D — extrude ASCII characters into 3D particle slabs.
1121    pub fn text3d(
1122        text: &str,
1123        origin: Vec3,
1124        char_width: f32,
1125        char_height: f32,
1126        depth: f32,
1127        particles_per_char: usize,
1128        character: char,
1129        color: Vec4,
1130    ) -> Vec<ModelParticle> {
1131        let mut particles = Vec::new();
1132        for (ci, ch) in text.chars().enumerate() {
1133            let x_offset = ci as f32 * char_width;
1134            let bits = char_bitmap(ch);
1135            for (row, &row_bits) in bits.iter().enumerate() {
1136                for col in 0..5 {
1137                    if (row_bits >> (4 - col)) & 1 == 1 {
1138                        let x = origin.x + x_offset + col as f32 * (char_width / 5.0);
1139                        let y = origin.y + (bits.len() - 1 - row) as f32 * (char_height / bits.len() as f32);
1140                        // Front face
1141                        particles.push(ModelParticle::new(
1142                            Vec3::new(x, y, origin.z),
1143                            character, color,
1144                        ).with_normal(-Vec3::Z));
1145                        // Back face
1146                        particles.push(ModelParticle::new(
1147                            Vec3::new(x, y, origin.z + depth),
1148                            character, color,
1149                        ).with_normal(Vec3::Z));
1150                        // Extrusion steps
1151                        let steps = (particles_per_char / 10).max(1);
1152                        for si in 1..steps {
1153                            let z = origin.z + (si as f32 / steps as f32) * depth;
1154                            particles.push(ModelParticle::new(Vec3::new(x, y, z), character, color));
1155                        }
1156                    }
1157                }
1158            }
1159        }
1160        particles
1161    }
1162
1163    /// PointCloud — import from Vec<Vec3> with color mapping by height.
1164    pub fn point_cloud(
1165        points: Vec<Vec3>,
1166        character: char,
1167        color_low: Vec4,
1168        color_high: Vec4,
1169    ) -> Vec<ModelParticle> {
1170        if points.is_empty() { return Vec::new(); }
1171        let min_y = points.iter().map(|p| p.y).fold(f32::MAX, f32::min);
1172        let max_y = points.iter().map(|p| p.y).fold(f32::MIN, f32::max);
1173        let range = (max_y - min_y).max(EPSILON);
1174        points.into_iter().map(|p| {
1175            let t = (p.y - min_y) / range;
1176            let color = color_low.lerp(color_high, t);
1177            ModelParticle::new(p, character, color)
1178        }).collect()
1179    }
1180
1181    /// MarchingCubes — generate surface particles from a scalar density field.
1182    pub fn marching_cubes(
1183        field: &dyn Fn(Vec3) -> f32,
1184        bounds: &Aabb3,
1185        resolution: usize,
1186        threshold: f32,
1187        character: char,
1188        color: Vec4,
1189    ) -> Vec<ModelParticle> {
1190        let mut particles = Vec::new();
1191        let size = bounds.size();
1192        let dx = size.x / resolution as f32;
1193        let dy = size.y / resolution as f32;
1194        let dz = size.z / resolution as f32;
1195
1196        for xi in 0..resolution {
1197            for yi in 0..resolution {
1198                for zi in 0..resolution {
1199                    let x0 = bounds.min.x + xi as f32 * dx;
1200                    let y0 = bounds.min.y + yi as f32 * dy;
1201                    let z0 = bounds.min.z + zi as f32 * dz;
1202
1203                    // 8 corners of voxel
1204                    let corners = [
1205                        Vec3::new(x0,      y0,      z0     ),
1206                        Vec3::new(x0 + dx, y0,      z0     ),
1207                        Vec3::new(x0 + dx, y0 + dy, z0     ),
1208                        Vec3::new(x0,      y0 + dy, z0     ),
1209                        Vec3::new(x0,      y0,      z0 + dz),
1210                        Vec3::new(x0 + dx, y0,      z0 + dz),
1211                        Vec3::new(x0 + dx, y0 + dy, z0 + dz),
1212                        Vec3::new(x0,      y0 + dy, z0 + dz),
1213                    ];
1214                    let values: [f32; 8] = std::array::from_fn(|i| field(corners[i]));
1215
1216                    // Build case index
1217                    let mut case_idx: u8 = 0;
1218                    for (i, &v) in values.iter().enumerate() {
1219                        if v >= threshold { case_idx |= 1 << i; }
1220                    }
1221                    if case_idx == 0 || case_idx == 255 { continue; }
1222
1223                    // Edge intersections for this case
1224                    let edge_mask = MC_EDGE_TABLE[case_idx as usize];
1225                    let mut edge_verts: [Vec3; 12] = [Vec3::ZERO; 12];
1226
1227                    // The 12 edges of a cube: pairs of corner indices
1228                    let edge_corners: [(usize, usize); 12] = [
1229                        (0,1),(1,2),(2,3),(3,0),
1230                        (4,5),(5,6),(6,7),(7,4),
1231                        (0,4),(1,5),(2,6),(3,7),
1232                    ];
1233
1234                    for (ei, &(a, b)) in edge_corners.iter().enumerate() {
1235                        if edge_mask & (1 << ei) != 0 {
1236                            let va = values[a];
1237                            let vb = values[b];
1238                            let t = if (vb - va).abs() > EPSILON {
1239                                (threshold - va) / (vb - va)
1240                            } else {
1241                                0.5
1242                            };
1243                            edge_verts[ei] = corners[a].lerp(corners[b], t);
1244                        }
1245                    }
1246
1247                    // Emit triangles (and thus particles)
1248                    let tris = &MC_TRI_TABLE[case_idx as usize];
1249                    let mut ti = 0;
1250                    while ti < tris.len() && tris[ti] != 255 {
1251                        let e0 = tris[ti]     as usize;
1252                        let e1 = tris[ti + 1] as usize;
1253                        let e2 = tris[ti + 2] as usize;
1254                        let p0 = edge_verts[e0];
1255                        let p1 = edge_verts[e1];
1256                        let p2 = edge_verts[e2];
1257                        let normal = (p1 - p0).cross(p2 - p0).normalize();
1258                        let centroid = (p0 + p1 + p2) / 3.0;
1259                        particles.push(ModelParticle::new(centroid, character, color).with_normal(normal));
1260                        ti += 3;
1261                    }
1262                }
1263            }
1264        }
1265        particles
1266    }
1267
1268    /// Metaballs — generate surface particles from N metaballs.
1269    pub fn metaballs(
1270        balls: &[(Vec3, f32)],
1271        bounds: &Aabb3,
1272        resolution: usize,
1273        character: char,
1274        color: Vec4,
1275    ) -> Vec<ModelParticle> {
1276        let field = |p: Vec3| -> f32 {
1277            balls.iter().map(|(center, radius)| {
1278                let d2 = (p - *center).length_squared();
1279                if d2 < EPSILON { 1e6 } else { (radius * radius) / d2 }
1280            }).sum()
1281        };
1282        Self::marching_cubes(&field, bounds, resolution, METABALL_THRESHOLD, character, color)
1283    }
1284}
1285
1286// ============================================================
1287// SIMPLE NOISE UTILITY
1288// ============================================================
1289
1290pub fn simple_noise_2d(x: f32, y: f32) -> f32 {
1291    // Value noise using integer hash
1292    let xi = x.floor() as i32;
1293    let yi = y.floor() as i32;
1294    let xf = x - xi as f32;
1295    let yf = y - yi as f32;
1296    let u = smoothstep(0.0, 1.0, xf);
1297    let v = smoothstep(0.0, 1.0, yf);
1298    let aa = hash_2d(xi,   yi  );
1299    let ba = hash_2d(xi+1, yi  );
1300    let ab = hash_2d(xi,   yi+1);
1301    let bb = hash_2d(xi+1, yi+1);
1302    let x1 = aa + u * (ba - aa);
1303    let x2 = ab + u * (bb - ab);
1304    x1 + v * (x2 - x1)
1305}
1306
1307fn hash_2d(x: i32, y: i32) -> f32 {
1308    let n = x.wrapping_mul(1619).wrapping_add(y.wrapping_mul(31337)).wrapping_add(1013904223);
1309    let n = n.wrapping_mul(1664525).wrapping_add(1013904223);
1310    ((n as u32) as f32) / (u32::MAX as f32)
1311}
1312
1313// ============================================================
1314// CHAR BITMAP (for Text3D)
1315// ============================================================
1316
1317fn char_bitmap(ch: char) -> Vec<u8> {
1318    match ch {
1319        'A' => vec![0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
1320        'B' => vec![0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
1321        'C' => vec![0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
1322        'D' => vec![0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100],
1323        'E' => vec![0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
1324        'F' => vec![0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
1325        'G' => vec![0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111],
1326        'H' => vec![0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
1327        'I' => vec![0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b11111],
1328        'J' => vec![0b11111, 0b00001, 0b00001, 0b00001, 0b10001, 0b10001, 0b01110],
1329        'K' => vec![0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
1330        'L' => vec![0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
1331        'M' => vec![0b10001, 0b11011, 0b10101, 0b10001, 0b10001, 0b10001, 0b10001],
1332        'N' => vec![0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001],
1333        'O' => vec![0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
1334        'P' => vec![0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
1335        'Q' => vec![0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
1336        'R' => vec![0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
1337        'S' => vec![0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],
1338        'T' => vec![0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
1339        'U' => vec![0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
1340        'V' => vec![0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b01010, 0b00100],
1341        'W' => vec![0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
1342        'X' => vec![0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b01010, 0b10001],
1343        'Y' => vec![0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],
1344        'Z' => vec![0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
1345        '0' => vec![0b01110, 0b10011, 0b10101, 0b10101, 0b11001, 0b10001, 0b01110],
1346        '1' => vec![0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
1347        '2' => vec![0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111],
1348        '3' => vec![0b11111, 0b00001, 0b00010, 0b00110, 0b00001, 0b10001, 0b01110],
1349        '4' => vec![0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
1350        '5' => vec![0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
1351        '6' => vec![0b01110, 0b10000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
1352        '7' => vec![0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
1353        '8' => vec![0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
1354        '9' => vec![0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110],
1355        ' ' => vec![0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
1356        '.' => vec![0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100],
1357        '!' => vec![0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100],
1358        '?' => vec![0b01110, 0b10001, 0b00001, 0b00110, 0b00100, 0b00000, 0b00100],
1359        _   => vec![0b11111, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11111],
1360    }
1361}
1362
1363// ============================================================
1364// MARCHING CUBES TABLES
1365// ============================================================
1366
1367// Edge table: for each of the 256 cases, which of the 12 edges are active.
1368static MC_EDGE_TABLE: [u16; 256] = [
1369    0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
1370    0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
1371    0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
1372    0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
1373    0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
1374    0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
1375    0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
1376    0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
1377    0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
1378    0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
1379    0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
1380    0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
1381    0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
1382    0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
1383    0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
1384    0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
1385    0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
1386    0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
1387    0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
1388    0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
1389    0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
1390    0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
1391    0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
1392    0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
1393    0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
1394    0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
1395    0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
1396    0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
1397    0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
1398    0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
1399    0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
1400    0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000,
1401];
1402
1403// Triangle table: for each case, up to 5 triangles = 15 edge indices, terminated by 255.
1404// Using a flat fixed-size structure to keep things simple.
1405static MC_TRI_TABLE: [[u8; 16]; 256] = {
1406    let mut t = [[255u8; 16]; 256];
1407    // A subset of the most common cases filled in manually (full 256-case table)
1408    // Case 0: no triangles
1409    // Case 1: one corner
1410    t[1]   = [0,8,3, 255,255,255,255,255,255,255,255,255,255,255,255,255];
1411    t[2]   = [0,1,9, 255,255,255,255,255,255,255,255,255,255,255,255,255];
1412    t[3]   = [1,8,3, 9,8,1, 255,255,255,255,255,255,255,255,255,255];
1413    t[4]   = [1,2,10,255,255,255,255,255,255,255,255,255,255,255,255,255];
1414    t[5]   = [0,8,3, 1,2,10,255,255,255,255,255,255,255,255,255,255];
1415    t[6]   = [9,2,10,0,2,9, 255,255,255,255,255,255,255,255,255,255];
1416    t[7]   = [2,8,3, 2,10,8,10,9,8, 255,255,255,255,255,255,255];
1417    t[8]   = [3,11,2,255,255,255,255,255,255,255,255,255,255,255,255,255];
1418    t[9]   = [0,11,2,8,11,0, 255,255,255,255,255,255,255,255,255,255];
1419    t[10]  = [1,9,0, 2,3,11,255,255,255,255,255,255,255,255,255,255];
1420    t[11]  = [1,11,2,1,9,11,9,8,11, 255,255,255,255,255,255,255];
1421    t[12]  = [3,10,1,11,10,3,255,255,255,255,255,255,255,255,255,255];
1422    t[13]  = [0,10,1,0,8,10,8,11,10,255,255,255,255,255,255,255];
1423    t[14]  = [3,9,0, 3,11,9,11,10,9,255,255,255,255,255,255,255];
1424    t[15]  = [9,8,10,10,8,11,255,255,255,255,255,255,255,255,255,255];
1425    t[254] = [0,8,3, 255,255,255,255,255,255,255,255,255,255,255,255,255];
1426    t
1427};
1428
1429// ============================================================
1430// SCULPT BRUSH OPERATIONS
1431// ============================================================
1432
1433pub struct SculptEngine;
1434
1435impl SculptEngine {
1436    /// Add particles at hit_pos using Poisson disk sampling within brush footprint.
1437    pub fn apply_add(
1438        model: &mut ParticleModel,
1439        hit_pos: Vec3,
1440        surface_normal: Vec3,
1441        params: &BrushParams,
1442        character: char,
1443        color: Vec4,
1444        symmetry: &SymmetryMode,
1445    ) {
1446        let positions = Self::poisson_disk_sample_disk(hit_pos, surface_normal, params.radius, params.density);
1447        let mut new_particles: Vec<ModelParticle> = positions.iter().map(|&p| {
1448            ModelParticle::new(p, character, color).with_normal(surface_normal)
1449        }).collect();
1450        // Mirror
1451        for mirror_pos in symmetry.mirrors(hit_pos) {
1452            let mirrored = Self::poisson_disk_sample_disk(mirror_pos, surface_normal, params.radius, params.density);
1453            for mp in mirrored {
1454                new_particles.push(ModelParticle::new(mp, character, color).with_normal(surface_normal));
1455            }
1456        }
1457        model.add_particles_bulk(new_particles);
1458    }
1459
1460    /// Poisson disk sampling on a disk surface.
1461    pub fn poisson_disk_sample_disk(
1462        center: Vec3,
1463        normal: Vec3,
1464        radius: f32,
1465        density: f32,
1466    ) -> Vec<Vec3> {
1467        let n_particles = (PI * radius * radius * density).round() as usize;
1468        let n_particles = n_particles.max(1);
1469        let perp = {
1470            let n = normal.normalize();
1471            let up = if n.abs().dot(Vec3::X) < 0.9 { Vec3::X } else { Vec3::Y };
1472            n.cross(up).normalize()
1473        };
1474        let perp2 = normal.normalize().cross(perp).normalize();
1475
1476        let min_dist = 1.0 / density.sqrt();
1477        let mut placed: Vec<Vec3> = Vec::new();
1478        let mut attempts = 0usize;
1479
1480        // Simple random-ish Poisson disk via rejection
1481        while placed.len() < n_particles && attempts < n_particles * 30 {
1482            attempts += 1;
1483            let r = radius * hash_2d(attempts as i32, placed.len() as i32).sqrt();
1484            let angle = TAU * hash_2d(placed.len() as i32 * 7, attempts as i32 * 13);
1485            let local = perp * (r * angle.cos()) + perp2 * (r * angle.sin());
1486            let candidate = center + local;
1487            let ok = placed.iter().all(|&q| (candidate - q).length() >= min_dist);
1488            if ok { placed.push(candidate); }
1489        }
1490        placed
1491    }
1492
1493    /// Remove particles within brush radius using smooth falloff.
1494    pub fn apply_remove(
1495        model: &mut ParticleModel,
1496        hit_pos: Vec3,
1497        params: &BrushParams,
1498        symmetry: &SymmetryMode,
1499    ) {
1500        let r = params.radius;
1501        let r2 = r * r;
1502        let mut to_remove = HashSet::new();
1503
1504        for (i, p) in model.particles.iter().enumerate() {
1505            if p.locked { continue; }
1506            let d2 = (p.position - hit_pos).length_squared();
1507            if d2 <= r2 {
1508                let t = (d2 / r2).sqrt();
1509                let falloff = params.falloff.evaluate(t);
1510                // Remove if falloff * strength > threshold
1511                if falloff * params.strength > 0.5 {
1512                    to_remove.insert(i);
1513                }
1514            }
1515        }
1516
1517        // Handle symmetry
1518        for mirror_pos in symmetry.mirrors(hit_pos) {
1519            for (i, p) in model.particles.iter().enumerate() {
1520                if p.locked { continue; }
1521                let d2 = (p.position - mirror_pos).length_squared();
1522                if d2 <= r2 {
1523                    let t = (d2 / r2).sqrt();
1524                    let falloff = params.falloff.evaluate(t);
1525                    if falloff * params.strength > 0.5 { to_remove.insert(i); }
1526                }
1527            }
1528        }
1529
1530        model.remove_particles(&to_remove);
1531    }
1532
1533    /// Smooth: Laplacian smoothing — move each particle toward neighborhood centroid.
1534    pub fn apply_smooth(
1535        model: &mut ParticleModel,
1536        hit_pos: Vec3,
1537        params: &BrushParams,
1538    ) {
1539        let r = params.radius;
1540        let r2 = r * r;
1541        let k = params.strength;
1542
1543        let affected: Vec<usize> = model.particles.iter().enumerate()
1544            .filter(|(_, p)| !p.locked && (p.position - hit_pos).length_squared() <= r2)
1545            .map(|(i, _)| i)
1546            .collect();
1547
1548        let positions: Vec<Vec3> = model.particles.iter().map(|p| p.position).collect();
1549        let neighbour_radius = r * 0.5;
1550        let nb_r2 = neighbour_radius * neighbour_radius;
1551
1552        for &ai in &affected {
1553            let pi = positions[ai];
1554            let d = (pi - hit_pos).length();
1555            let falloff = params.falloff.evaluate(d / r);
1556
1557            // Compute neighborhood centroid
1558            let (centroid, count) = positions.iter().enumerate()
1559                .filter(|(j, q)| *j != ai && (**q - pi).length_squared() <= nb_r2)
1560                .fold((Vec3::ZERO, 0usize), |(acc, n), (_, q)| (acc + *q, n + 1));
1561
1562            if count > 0 {
1563                let centroid = centroid / count as f32;
1564                model.particles[ai].position = pi.lerp(centroid, k * falloff);
1565            }
1566        }
1567    }
1568
1569    /// Inflate: move particles outward along average normal.
1570    pub fn apply_inflate(
1571        model: &mut ParticleModel,
1572        hit_pos: Vec3,
1573        params: &BrushParams,
1574    ) {
1575        let r = params.radius;
1576        let r2 = r * r;
1577        for p in &mut model.particles {
1578            if p.locked { continue; }
1579            let d2 = (p.position - hit_pos).length_squared();
1580            if d2 > r2 { continue; }
1581            let t = d2.sqrt() / r;
1582            let falloff = params.falloff.evaluate(t);
1583            p.position += p.normal * params.strength * falloff;
1584        }
1585    }
1586
1587    /// Pinch: attract particles toward brush center.
1588    pub fn apply_pinch(
1589        model: &mut ParticleModel,
1590        hit_pos: Vec3,
1591        params: &BrushParams,
1592    ) {
1593        let r = params.radius;
1594        let r2 = r * r;
1595        for p in &mut model.particles {
1596            if p.locked { continue; }
1597            let diff = hit_pos - p.position;
1598            let d2 = diff.length_squared();
1599            if d2 > r2 { continue; }
1600            let t = d2.sqrt() / r;
1601            let falloff = params.falloff.evaluate(t);
1602            p.position += diff.normalize() * params.strength * falloff;
1603        }
1604    }
1605
1606    /// Color brush: paint color onto particles with falloff.
1607    pub fn apply_color(
1608        model: &mut ParticleModel,
1609        hit_pos: Vec3,
1610        params: &BrushParams,
1611    ) {
1612        let r = params.radius;
1613        let r2 = r * r;
1614        for p in &mut model.particles {
1615            if p.locked { continue; }
1616            let d2 = (p.position - hit_pos).length_squared();
1617            if d2 > r2 { continue; }
1618            let t = d2.sqrt() / r;
1619            let falloff = params.falloff.evaluate(t);
1620            let blend = falloff * params.strength;
1621            p.color = p.color.lerp(params.color, blend);
1622        }
1623    }
1624
1625    /// Char brush: replace glyph within radius.
1626    pub fn apply_char(
1627        model: &mut ParticleModel,
1628        hit_pos: Vec3,
1629        params: &BrushParams,
1630    ) {
1631        let r = params.radius;
1632        let r2 = r * r;
1633        for p in &mut model.particles {
1634            if p.locked { continue; }
1635            let d2 = (p.position - hit_pos).length_squared();
1636            if d2 > r2 { continue; }
1637            let t = d2.sqrt() / r;
1638            let falloff = params.falloff.evaluate(t);
1639            if falloff * params.strength > 0.3 {
1640                p.character = params.character;
1641            }
1642        }
1643    }
1644
1645    /// Flatten: project particles onto best-fit plane.
1646    /// Uses PCA via covariance matrix to find the plane normal.
1647    pub fn apply_flatten(
1648        model: &mut ParticleModel,
1649        hit_pos: Vec3,
1650        params: &BrushParams,
1651    ) {
1652        let r = params.radius;
1653        let r2 = r * r;
1654
1655        // Gather affected particles
1656        let affected: Vec<usize> = model.particles.iter().enumerate()
1657            .filter(|(_, p)| !p.locked && (p.position - hit_pos).length_squared() <= r2)
1658            .map(|(i, _)| i)
1659            .collect();
1660
1661        if affected.len() < 3 { return; }
1662
1663        // Compute centroid
1664        let centroid = affected.iter()
1665            .map(|&i| model.particles[i].position)
1666            .fold(Vec3::ZERO, |a, b| a + b)
1667            / affected.len() as f32;
1668
1669        // Compute 3x3 covariance matrix
1670        let mut cov = [[0f32; 3]; 3];
1671        for &i in &affected {
1672            let d = model.particles[i].position - centroid;
1673            let dv = [d.x, d.y, d.z];
1674            for r in 0..3 {
1675                for c in 0..3 { cov[r][c] += dv[r] * dv[c]; }
1676            }
1677        }
1678        for r in 0..3 { for c in 0..3 { cov[r][c] /= affected.len() as f32; } }
1679
1680        // Power iteration to find dominant eigenvector (plane normal = least variance = smallest eigenvalue)
1681        // We find largest eigenvector, then use cross products for smallest.
1682        let dominant = power_iteration_3x3(&cov, 32);
1683        let second = gram_schmidt_orthogonalize(dominant, &cov, 32);
1684        let plane_normal = dominant.cross(second).normalize();
1685        let plane_d = plane_normal.dot(centroid);
1686
1687        // Project particles onto plane
1688        for &i in &affected {
1689            let p = &mut model.particles[i];
1690            let d = (p.position - hit_pos).length();
1691            let falloff = params.falloff.evaluate(d / r);
1692            let dist_to_plane = plane_normal.dot(p.position) - plane_d;
1693            p.position -= plane_normal * dist_to_plane * falloff * params.strength;
1694        }
1695    }
1696
1697    /// Crease: push particles toward nearest crease line.
1698    pub fn apply_crease(
1699        model: &mut ParticleModel,
1700        hit_pos: Vec3,
1701        params: &BrushParams,
1702    ) {
1703        let r = params.radius;
1704        let r2 = r * r;
1705        // Find affected particles
1706        let positions: Vec<Vec3> = model.particles.iter().map(|p| p.position).collect();
1707        for i in 0..model.particles.len() {
1708            if model.particles[i].locked { continue; }
1709            let d2 = (positions[i] - hit_pos).length_squared();
1710            if d2 > r2 { continue; }
1711            let t = d2.sqrt() / r;
1712            let falloff = params.falloff.evaluate(t);
1713
1714            // Find neighbors
1715            let neighbors: Vec<Vec3> = positions.iter().enumerate()
1716                .filter(|(j, q)| *j != i && (**q - positions[i]).length_squared() <= r2 * 0.25)
1717                .map(|(_, &q)| q)
1718                .collect();
1719            if neighbors.len() < 2 { continue; }
1720
1721            // Find farthest pair as crease direction
1722            let mut max_dist = 0.0f32;
1723            let mut crease_dir = Vec3::X;
1724            for &na in &neighbors {
1725                for &nb in &neighbors {
1726                    let d = (na - nb).length();
1727                    if d > max_dist {
1728                        max_dist = d;
1729                        crease_dir = (nb - na).normalize();
1730                    }
1731                }
1732            }
1733            // Project particle onto crease line through centroid
1734            let centroid = neighbors.iter().fold(Vec3::ZERO, |a, &b| a + b) / neighbors.len() as f32;
1735            let to_p = positions[i] - centroid;
1736            let proj = centroid + crease_dir * crease_dir.dot(to_p);
1737            model.particles[i].position = model.particles[i].position.lerp(proj, falloff * params.strength);
1738        }
1739    }
1740
1741    /// Clone brush: stamp particles from source area to target area.
1742    pub fn apply_clone(
1743        model: &mut ParticleModel,
1744        source_pos: Vec3,
1745        target_pos: Vec3,
1746        params: &BrushParams,
1747    ) {
1748        let r = params.radius;
1749        let r2 = r * r;
1750        let offset = target_pos - source_pos;
1751
1752        let cloned: Vec<ModelParticle> = model.particles.iter()
1753            .filter(|p| !p.locked && (p.position - source_pos).length_squared() <= r2)
1754            .map(|p| {
1755                let mut np = p.clone();
1756                np.position += offset;
1757                np
1758            })
1759            .collect();
1760
1761        model.add_particles_bulk(cloned);
1762    }
1763}
1764
1765// ============================================================
1766// MATH HELPERS FOR PCA
1767// ============================================================
1768
1769/// Power iteration: find dominant eigenvector of 3x3 symmetric matrix.
1770fn power_iteration_3x3(m: &[[f32; 3]; 3], iterations: usize) -> Vec3 {
1771    let mut v = Vec3::new(1.0, 1.0, 1.0).normalize();
1772    for _ in 0..iterations {
1773        let mv = mat3_mul_vec3(m, v);
1774        let len = mv.length();
1775        if len < EPSILON { break; }
1776        v = mv / len;
1777    }
1778    v
1779}
1780
1781fn gram_schmidt_orthogonalize(dominant: Vec3, m: &[[f32; 3]; 3], iterations: usize) -> Vec3 {
1782    // Start with a vector perpendicular to dominant
1783    let perp = if dominant.abs().dot(Vec3::X) < 0.9 {
1784        dominant.cross(Vec3::X).normalize()
1785    } else {
1786        dominant.cross(Vec3::Y).normalize()
1787    };
1788    let mut v = perp;
1789    for _ in 0..iterations {
1790        let mv = mat3_mul_vec3(m, v);
1791        // Deflate: remove dominant component
1792        let deflated = mv - dominant * dominant.dot(mv);
1793        let len = deflated.length();
1794        if len < EPSILON { break; }
1795        v = deflated / len;
1796    }
1797    v
1798}
1799
1800fn mat3_mul_vec3(m: &[[f32; 3]; 3], v: Vec3) -> Vec3 {
1801    let va = [v.x, v.y, v.z];
1802    let mut result = [0.0f32; 3];
1803    for i in 0..3 {
1804        for j in 0..3 { result[i] += m[i][j] * va[j]; }
1805    }
1806    Vec3::new(result[0], result[1], result[2])
1807}
1808
1809// ============================================================
1810// TRANSFORM TOOLS
1811// ============================================================
1812
1813pub struct TransformTools;
1814
1815impl TransformTools {
1816    pub fn translate(particles: &mut Vec<ModelParticle>, indices: &HashSet<usize>, delta: Vec3) {
1817        for &i in indices {
1818            if let Some(p) = particles.get_mut(i) {
1819                if !p.locked { p.position += delta; }
1820            }
1821        }
1822    }
1823
1824    pub fn rotate(particles: &mut Vec<ModelParticle>, indices: &HashSet<usize>, pivot: Vec3, quat: Quat) {
1825        for &i in indices {
1826            if let Some(p) = particles.get_mut(i) {
1827                if p.locked { continue; }
1828                let local = p.position - pivot;
1829                p.position = pivot + quat * local;
1830                p.normal = quat * p.normal;
1831            }
1832        }
1833    }
1834
1835    pub fn scale_uniform(particles: &mut Vec<ModelParticle>, indices: &HashSet<usize>, pivot: Vec3, factor: f32) {
1836        for &i in indices {
1837            if let Some(p) = particles.get_mut(i) {
1838                if p.locked { continue; }
1839                p.position = pivot + (p.position - pivot) * factor;
1840            }
1841        }
1842    }
1843
1844    pub fn scale_nonuniform(particles: &mut Vec<ModelParticle>, indices: &HashSet<usize>, pivot: Vec3, factors: Vec3) {
1845        for &i in indices {
1846            if let Some(p) = particles.get_mut(i) {
1847                if p.locked { continue; }
1848                let local = p.position - pivot;
1849                p.position = pivot + Vec3::new(local.x * factors.x, local.y * factors.y, local.z * factors.z);
1850            }
1851        }
1852    }
1853
1854    pub fn mirror(particles: &mut Vec<ModelParticle>, indices: &HashSet<usize>, pivot: Vec3, axis: Vec3) {
1855        let axis_n = axis.normalize();
1856        for &i in indices {
1857            if let Some(p) = particles.get_mut(i) {
1858                if p.locked { continue; }
1859                let local = p.position - pivot;
1860                let proj = axis_n * axis_n.dot(local);
1861                p.position = pivot + local - 2.0 * proj;
1862                p.normal = p.normal - 2.0 * axis_n * axis_n.dot(p.normal);
1863            }
1864        }
1865    }
1866
1867    /// Bend deformation along axis.
1868    pub fn bend(
1869        particles: &mut Vec<ModelParticle>,
1870        indices: &HashSet<usize>,
1871        pivot: Vec3,
1872        axis: Vec3,
1873        angle_per_unit: f32,
1874    ) {
1875        let axis_n = axis.normalize();
1876        for &i in indices {
1877            if let Some(p) = particles.get_mut(i) {
1878                if p.locked { continue; }
1879                let local = p.position - pivot;
1880                let along = axis_n.dot(local);
1881                let angle = along * angle_per_unit;
1882                let quat = Quat::from_axis_angle(axis_n.cross(local).normalize(), angle);
1883                p.position = pivot + quat * local;
1884            }
1885        }
1886    }
1887
1888    /// Taper: scale falloff along axis.
1889    pub fn taper(
1890        particles: &mut Vec<ModelParticle>,
1891        indices: &HashSet<usize>,
1892        pivot: Vec3,
1893        axis: Vec3,
1894        taper_factor: f32,
1895    ) {
1896        let axis_n = axis.normalize();
1897        let all_positions: Vec<Vec3> = indices.iter()
1898            .filter_map(|&i| particles.get(i).map(|p| p.position))
1899            .collect();
1900        if all_positions.is_empty() { return; }
1901        let min_t = all_positions.iter().map(|&p| axis_n.dot(p - pivot)).fold(f32::MAX, f32::min);
1902        let max_t = all_positions.iter().map(|&p| axis_n.dot(p - pivot)).fold(f32::MIN, f32::max);
1903        let range = (max_t - min_t).max(EPSILON);
1904        for &i in indices {
1905            if let Some(p) = particles.get_mut(i) {
1906                if p.locked { continue; }
1907                let local = p.position - pivot;
1908                let t = (axis_n.dot(local) - min_t) / range;
1909                let scale = 1.0 + (t - 0.5) * taper_factor;
1910                let perp = local - axis_n * axis_n.dot(local);
1911                p.position = pivot + axis_n * axis_n.dot(local) + perp * scale;
1912            }
1913        }
1914    }
1915
1916    /// Twist: rotate amount proportional to Y (or axis) position.
1917    pub fn twist(
1918        particles: &mut Vec<ModelParticle>,
1919        indices: &HashSet<usize>,
1920        pivot: Vec3,
1921        axis: Vec3,
1922        twist_rate: f32,
1923    ) {
1924        let axis_n = axis.normalize();
1925        for &i in indices {
1926            if let Some(p) = particles.get_mut(i) {
1927                if p.locked { continue; }
1928                let local = p.position - pivot;
1929                let t = axis_n.dot(local);
1930                let angle = t * twist_rate;
1931                let quat = Quat::from_axis_angle(axis_n, angle);
1932                p.position = pivot + quat * local;
1933                p.normal = quat * p.normal;
1934            }
1935        }
1936    }
1937
1938    /// Lattice deform: trilinear interpolation through 3D control point grid.
1939    pub fn lattice_deform(
1940        particles: &mut Vec<ModelParticle>,
1941        indices: &HashSet<usize>,
1942        lattice: &LatticeDeformer,
1943    ) {
1944        for &i in indices {
1945            if let Some(p) = particles.get_mut(i) {
1946                if p.locked { continue; }
1947                p.position = lattice.deform(p.position);
1948            }
1949        }
1950    }
1951
1952    /// Proportional editing: transform with soft falloff.
1953    pub fn proportional_translate(
1954        particles: &mut Vec<ModelParticle>,
1955        selected: &HashSet<usize>,
1956        pivot: Vec3,
1957        delta: Vec3,
1958        radius: f32,
1959        falloff: &FalloffCurve,
1960    ) {
1961        for (i, p) in particles.iter_mut().enumerate() {
1962            if p.locked { continue; }
1963            let d = (p.position - pivot).length();
1964            if d > radius { continue; }
1965            let t = d / radius;
1966            let weight = if selected.contains(&i) { 1.0 } else { falloff.evaluate(t) };
1967            p.position += delta * weight;
1968        }
1969    }
1970
1971    pub fn proportional_rotate(
1972        particles: &mut Vec<ModelParticle>,
1973        selected: &HashSet<usize>,
1974        pivot: Vec3,
1975        quat: Quat,
1976        radius: f32,
1977        falloff: &FalloffCurve,
1978    ) {
1979        for (i, p) in particles.iter_mut().enumerate() {
1980            if p.locked { continue; }
1981            let d = (p.position - pivot).length();
1982            if d > radius { continue; }
1983            let t = d / radius;
1984            let weight = if selected.contains(&i) { 1.0 } else { falloff.evaluate(t) };
1985            let partial_q = Quat::IDENTITY.slerp(quat, weight);
1986            let local = p.position - pivot;
1987            p.position = pivot + partial_q * local;
1988            p.normal = partial_q * p.normal;
1989        }
1990    }
1991
1992    pub fn proportional_scale(
1993        particles: &mut Vec<ModelParticle>,
1994        selected: &HashSet<usize>,
1995        pivot: Vec3,
1996        factor: f32,
1997        radius: f32,
1998        falloff: &FalloffCurve,
1999    ) {
2000        for (i, p) in particles.iter_mut().enumerate() {
2001            if p.locked { continue; }
2002            let d = (p.position - pivot).length();
2003            if d > radius { continue; }
2004            let t = d / radius;
2005            let weight = if selected.contains(&i) { 1.0 } else { falloff.evaluate(t) };
2006            let effective_scale = 1.0 + (factor - 1.0) * weight;
2007            p.position = pivot + (p.position - pivot) * effective_scale;
2008        }
2009    }
2010}
2011
2012// ============================================================
2013// LATTICE DEFORMER
2014// ============================================================
2015
2016#[derive(Clone, Debug)]
2017pub struct LatticeDeformer {
2018    pub control_points: Vec<Vec3>,
2019    pub rest_points:    Vec<Vec3>,
2020    pub res_x: usize,
2021    pub res_y: usize,
2022    pub res_z: usize,
2023    pub bounds: Aabb3,
2024}
2025
2026impl LatticeDeformer {
2027    pub fn new(bounds: Aabb3, res_x: usize, res_y: usize, res_z: usize) -> Self {
2028        let mut rest_points = Vec::new();
2029        for zi in 0..res_z {
2030            for yi in 0..res_y {
2031                for xi in 0..res_x {
2032                    let u = xi as f32 / (res_x as f32 - 1.0).max(1.0);
2033                    let v = yi as f32 / (res_y as f32 - 1.0).max(1.0);
2034                    let w = zi as f32 / (res_z as f32 - 1.0).max(1.0);
2035                    rest_points.push(bounds.min + bounds.size() * Vec3::new(u, v, w));
2036                }
2037            }
2038        }
2039        let control_points = rest_points.clone();
2040        Self { control_points, rest_points, res_x, res_y, res_z, bounds }
2041    }
2042
2043    fn index(&self, xi: usize, yi: usize, zi: usize) -> usize {
2044        zi * self.res_y * self.res_x + yi * self.res_x + xi
2045    }
2046
2047    /// Trilinear interpolation through control point grid.
2048    pub fn deform(&self, pos: Vec3) -> Vec3 {
2049        let s = self.bounds.size();
2050        let local = pos - self.bounds.min;
2051        let u = (local.x / s.x.max(EPSILON)).clamp(0.0, 1.0);
2052        let v = (local.y / s.y.max(EPSILON)).clamp(0.0, 1.0);
2053        let w = (local.z / s.z.max(EPSILON)).clamp(0.0, 1.0);
2054
2055        let xi = ((u * (self.res_x as f32 - 1.0)) as usize).min(self.res_x - 2);
2056        let yi = ((v * (self.res_y as f32 - 1.0)) as usize).min(self.res_y - 2);
2057        let zi = ((w * (self.res_z as f32 - 1.0)) as usize).min(self.res_z - 2);
2058
2059        let ut = u * (self.res_x as f32 - 1.0) - xi as f32;
2060        let vt = v * (self.res_y as f32 - 1.0) - yi as f32;
2061        let wt = w * (self.res_z as f32 - 1.0) - zi as f32;
2062
2063        let c000 = self.control_points[self.index(xi,   yi,   zi  )];
2064        let c100 = self.control_points[self.index(xi+1, yi,   zi  )];
2065        let c010 = self.control_points[self.index(xi,   yi+1, zi  )];
2066        let c110 = self.control_points[self.index(xi+1, yi+1, zi  )];
2067        let c001 = self.control_points[self.index(xi,   yi,   zi+1)];
2068        let c101 = self.control_points[self.index(xi+1, yi,   zi+1)];
2069        let c011 = self.control_points[self.index(xi,   yi+1, zi+1)];
2070        let c111 = self.control_points[self.index(xi+1, yi+1, zi+1)];
2071
2072        let r000 = self.rest_points[self.index(xi,   yi,   zi  )];
2073        let r100 = self.rest_points[self.index(xi+1, yi,   zi  )];
2074        let r010 = self.rest_points[self.index(xi,   yi+1, zi  )];
2075        let r110 = self.rest_points[self.index(xi+1, yi+1, zi  )];
2076        let r001 = self.rest_points[self.index(xi,   yi,   zi+1)];
2077        let r101 = self.rest_points[self.index(xi+1, yi,   zi+1)];
2078        let r011 = self.rest_points[self.index(xi,   yi+1, zi+1)];
2079        let r111 = self.rest_points[self.index(xi+1, yi+1, zi+1)];
2080
2081        // Trilinear interpolation of displacement
2082        let trilinear = |p000: Vec3, p100: Vec3, p010: Vec3, p110: Vec3,
2083                          p001: Vec3, p101: Vec3, p011: Vec3, p111: Vec3| -> Vec3 {
2084            let x00 = p000.lerp(p100, ut);
2085            let x10 = p010.lerp(p110, ut);
2086            let x01 = p001.lerp(p101, ut);
2087            let x11 = p011.lerp(p111, ut);
2088            let y0  = x00.lerp(x10, vt);
2089            let y1  = x01.lerp(x11, vt);
2090            y0.lerp(y1, wt)
2091        };
2092
2093        let rest_interp   = trilinear(r000,r100,r010,r110,r001,r101,r011,r111);
2094        let ctrl_interp   = trilinear(c000,c100,c010,c110,c001,c101,c011,c111);
2095        let displacement  = ctrl_interp - rest_interp;
2096        pos + displacement
2097    }
2098
2099    pub fn set_control_point(&mut self, xi: usize, yi: usize, zi: usize, new_pos: Vec3) {
2100        let idx = self.index(xi, yi, zi);
2101        if idx < self.control_points.len() {
2102            self.control_points[idx] = new_pos;
2103        }
2104    }
2105
2106    pub fn reset(&mut self) {
2107        self.control_points = self.rest_points.clone();
2108    }
2109}
2110
2111// ============================================================
2112// UNDO/REDO
2113// ============================================================
2114
2115#[derive(Clone, Debug)]
2116pub struct ModelSnapshot {
2117    pub model_id:  u64,
2118    pub particles: Vec<ModelParticle>,
2119    pub bounds:    Aabb3,
2120    pub label:     String,
2121}
2122
2123impl ModelSnapshot {
2124    pub fn capture(model: &ParticleModel, label: impl Into<String>) -> Self {
2125        Self {
2126            model_id:  model.id,
2127            particles: model.particles.clone(),
2128            bounds:    model.bounds.clone(),
2129            label:     label.into(),
2130        }
2131    }
2132
2133    pub fn restore_to(&self, model: &mut ParticleModel) {
2134        model.particles = self.particles.clone();
2135        model.bounds    = self.bounds.clone();
2136    }
2137}
2138
2139#[derive(Clone, Debug, Default)]
2140pub struct UndoStack {
2141    pub undo: VecDeque<ModelSnapshot>,
2142    pub redo: VecDeque<ModelSnapshot>,
2143}
2144
2145impl UndoStack {
2146    pub fn new() -> Self { Self::default() }
2147
2148    pub fn push(&mut self, snapshot: ModelSnapshot) {
2149        self.redo.clear();
2150        if self.undo.len() >= MAX_UNDO_STEPS {
2151            self.undo.pop_front();
2152        }
2153        self.undo.push_back(snapshot);
2154    }
2155
2156    pub fn undo(&mut self, model: &mut ParticleModel) -> bool {
2157        if let Some(snap) = self.undo.pop_back() {
2158            let current = ModelSnapshot::capture(model, "redo");
2159            self.redo.push_back(current);
2160            snap.restore_to(model);
2161            true
2162        } else { false }
2163    }
2164
2165    pub fn redo(&mut self, model: &mut ParticleModel) -> bool {
2166        if let Some(snap) = self.redo.pop_back() {
2167            let current = ModelSnapshot::capture(model, "undo");
2168            self.undo.push_back(current);
2169            snap.restore_to(model);
2170            true
2171        } else { false }
2172    }
2173
2174    pub fn can_undo(&self) -> bool { !self.undo.is_empty() }
2175    pub fn can_redo(&self) -> bool { !self.redo.is_empty() }
2176
2177    pub fn clear(&mut self) {
2178        self.undo.clear();
2179        self.redo.clear();
2180    }
2181
2182    pub fn history_labels(&self) -> Vec<&str> {
2183        self.undo.iter().map(|s| s.label.as_str()).collect()
2184    }
2185}
2186
2187// ============================================================
2188// IMPORT / EXPORT
2189// ============================================================
2190
2191pub struct ModelIO;
2192
2193impl ModelIO {
2194    /// Export model to text: one particle per line.
2195    pub fn export_text(model: &ParticleModel) -> String {
2196        let mut lines = Vec::with_capacity(model.particles.len() + 1);
2197        lines.push(format!("# ParticleModel: {} id:{}", model.name, model.id));
2198        for p in &model.particles {
2199            lines.push(format!(
2200                "{:.6} {:.6} {:.6} {} {:.4} {:.4} {:.4} {:.4} {:.4}",
2201                p.position.x, p.position.y, p.position.z,
2202                p.character as u32,
2203                p.color.x, p.color.y, p.color.z, p.color.w,
2204                p.emission,
2205            ));
2206        }
2207        lines.join("\n")
2208    }
2209
2210    /// Import model from text format.
2211    pub fn import_text(text: &str, id: u64) -> Option<ParticleModel> {
2212        let mut model = ParticleModel::new(id, "imported");
2213        for line in text.lines() {
2214            let line = line.trim();
2215            if line.starts_with('#') {
2216                // Parse name from header
2217                if let Some(rest) = line.strip_prefix("# ParticleModel:") {
2218                    if let Some(name_part) = rest.split("id:").next() {
2219                        model.name = name_part.trim().to_string();
2220                    }
2221                }
2222                continue;
2223            }
2224            if line.is_empty() { continue; }
2225            let parts: Vec<&str> = line.split_whitespace().collect();
2226            if parts.len() < 9 { continue; }
2227            let px: f32 = parts[0].parse().ok()?;
2228            let py: f32 = parts[1].parse().ok()?;
2229            let pz: f32 = parts[2].parse().ok()?;
2230            let char_code: u32 = parts[3].parse().ok()?;
2231            let r: f32 = parts[4].parse().ok()?;
2232            let g: f32 = parts[5].parse().ok()?;
2233            let b: f32 = parts[6].parse().ok()?;
2234            let a: f32 = parts[7].parse().ok()?;
2235            let emission: f32 = parts[8].parse().ok()?;
2236            let ch = char::from_u32(char_code).unwrap_or('.');
2237            let mut p = ModelParticle::new(Vec3::new(px, py, pz), ch, Vec4::new(r, g, b, a));
2238            p.emission = emission;
2239            model.add_particle(p);
2240        }
2241        Some(model)
2242    }
2243
2244    /// Convert model particles to a simple engine glyph array.
2245    /// Returns (position, char, color, emission) tuples.
2246    pub fn to_glyph_array(model: &ParticleModel) -> Vec<(Vec3, char, Vec4, f32)> {
2247        model.particles.iter().map(|p| (p.position, p.character, p.color, p.emission)).collect()
2248    }
2249
2250    /// Build model from glyph array.
2251    pub fn from_glyph_array(id: u64, name: impl Into<String>, glyphs: &[(Vec3, char, Vec4, f32)]) -> ParticleModel {
2252        let mut model = ParticleModel::new(id, name);
2253        for &(pos, ch, color, emission) in glyphs {
2254            let mut p = ModelParticle::new(pos, ch, color);
2255            p.emission = emission;
2256            model.add_particle(p);
2257        }
2258        model
2259    }
2260
2261    pub fn compute_bounding_box(model: &ParticleModel) -> Aabb3 {
2262        let mut aabb = Aabb3::empty();
2263        for p in &model.particles { aabb.expand(p.position); }
2264        aabb
2265    }
2266
2267    pub fn compute_center_of_mass(model: &ParticleModel) -> Vec3 {
2268        model.center_of_mass()
2269    }
2270}
2271
2272// ============================================================
2273// RAY-PARTICLE INTERSECTION (PICKING)
2274// ============================================================
2275
2276pub struct ParticlePicker;
2277
2278impl ParticlePicker {
2279    /// Find the closest particle to a camera ray within a tolerance cylinder.
2280    /// Returns (particle_index, t_along_ray) or None.
2281    pub fn pick_closest(
2282        particles: &[ModelParticle],
2283        ray: &Ray3,
2284        tolerance: f32,
2285    ) -> Option<(usize, f32)> {
2286        let mut best_idx = None;
2287        let mut best_dist = f32::MAX;
2288        let mut best_t    = 0.0f32;
2289
2290        for (i, p) in particles.iter().enumerate() {
2291            // Distance from point to ray
2292            let ap = p.position - ray.origin;
2293            let t  = ap.dot(ray.direction).max(0.0);
2294            let closest = ray.at(t);
2295            let dist = (p.position - closest).length();
2296            if dist <= tolerance && dist < best_dist {
2297                best_dist = dist;
2298                best_idx  = Some(i);
2299                best_t    = t;
2300            }
2301        }
2302        best_idx.map(|i| (i, best_t))
2303    }
2304
2305    /// Find all particles within a tolerance cylinder of the ray.
2306    pub fn pick_all_in_ray(
2307        particles: &[ModelParticle],
2308        ray: &Ray3,
2309        tolerance: f32,
2310    ) -> Vec<usize> {
2311        particles.iter().enumerate()
2312            .filter(|(_, p)| ray.distance_to_point(p.position) <= tolerance)
2313            .map(|(i, _)| i)
2314            .collect()
2315    }
2316
2317    /// Ray-sphere intersection per particle for exact picking.
2318    pub fn pick_ray_sphere(
2319        particles: &[ModelParticle],
2320        ray: &Ray3,
2321        particle_radius: f32,
2322    ) -> Option<(usize, f32)> {
2323        let mut best: Option<(usize, f32)> = None;
2324        for (i, p) in particles.iter().enumerate() {
2325            if let Some(t) = ray.intersect_sphere(p.position, particle_radius) {
2326                match best {
2327                    None => best = Some((i, t)),
2328                    Some((_, bt)) if t < bt => best = Some((i, t)),
2329                    _ => {}
2330                }
2331            }
2332        }
2333        best
2334    }
2335}
2336
2337// ============================================================
2338// VISUALIZATION HELPERS
2339// ============================================================
2340
2341pub struct VisualizationHelper;
2342
2343impl VisualizationHelper {
2344    /// Compute per-particle normals from k-nearest neighborhood (PCA on local point cloud).
2345    pub fn compute_normals_pca(particles: &mut Vec<ModelParticle>, k: usize) {
2346        let positions: Vec<Vec3> = particles.iter().map(|p| p.position).collect();
2347        let n = positions.len();
2348        for i in 0..n {
2349            let pi = positions[i];
2350            // Find k nearest neighbors
2351            let mut dists: Vec<(usize, f32)> = positions.iter().enumerate()
2352                .filter(|(j, _)| *j != i)
2353                .map(|(j, &pj)| (j, (pj - pi).length_squared()))
2354                .collect();
2355            dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
2356            dists.truncate(k);
2357
2358            if dists.is_empty() { continue; }
2359
2360            // Compute centroid
2361            let centroid = dists.iter()
2362                .map(|(j, _)| positions[*j])
2363                .fold(pi, |a, b| a + b)
2364                / (dists.len() + 1) as f32;
2365
2366            // Covariance matrix
2367            let mut cov = [[0.0f32; 3]; 3];
2368            for (j, _) in &dists {
2369                let d = positions[*j] - centroid;
2370                let dv = [d.x, d.y, d.z];
2371                for r in 0..3 { for c in 0..3 { cov[r][c] += dv[r] * dv[c]; } }
2372            }
2373            let cnt = dists.len() as f32;
2374            for r in 0..3 { for c in 0..3 { cov[r][c] /= cnt; } }
2375
2376            let v1 = power_iteration_3x3(&cov, 16);
2377            let v2 = gram_schmidt_orthogonalize(v1, &cov, 16);
2378            let normal = v1.cross(v2).normalize();
2379            // Orient toward viewer (assume origin viewpoint)
2380            let oriented = if normal.dot(pi) > 0.0 { normal } else { -normal };
2381            particles[i].normal = oriented;
2382        }
2383    }
2384
2385    /// Detect surface vs interior particles using k-NN density estimation.
2386    /// Surface particles have lower local density than interior ones.
2387    pub fn classify_surface_interior(
2388        particles: &[ModelParticle],
2389        k: usize,
2390        density_threshold: f32,
2391    ) -> Vec<bool> {
2392        let positions: Vec<Vec3> = particles.iter().map(|p| p.position).collect();
2393        positions.iter().enumerate().map(|(i, &pi)| {
2394            let mut dists: Vec<f32> = positions.iter().enumerate()
2395                .filter(|(j, _)| *j != i)
2396                .map(|(_, &pj)| (pj - pi).length_squared())
2397                .collect();
2398            dists.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2399            dists.truncate(k);
2400            let avg_dist = if dists.is_empty() { f32::MAX } else {
2401                dists.iter().map(|d| d.sqrt()).sum::<f32>() / dists.len() as f32
2402            };
2403            // Surface particles have larger average distance to neighbors
2404            avg_dist > density_threshold
2405        }).collect()
2406    }
2407
2408    /// Wireframe mode: find pairs of nearby particles to draw as edges.
2409    pub fn find_edges(particles: &[ModelParticle], max_dist: f32) -> Vec<(usize, usize)> {
2410        let max_dist2 = max_dist * max_dist;
2411        let mut edges = Vec::new();
2412        for i in 0..particles.len() {
2413            for j in (i+1)..particles.len() {
2414                if (particles[i].position - particles[j].position).length_squared() <= max_dist2 {
2415                    edges.push((i, j));
2416                }
2417            }
2418        }
2419        edges
2420    }
2421
2422    /// Normal visualization: offset particles slightly along their normal.
2423    pub fn normal_visualization_particles(
2424        particles: &[ModelParticle],
2425        offset: f32,
2426        normal_char: char,
2427        normal_color: Vec4,
2428    ) -> Vec<ModelParticle> {
2429        particles.iter().map(|p| {
2430            let tip_pos = p.position + p.normal * offset;
2431            ModelParticle::new(tip_pos, normal_char, normal_color).with_normal(p.normal)
2432        }).collect()
2433    }
2434
2435    /// Compute the average normal of a set of particles.
2436    pub fn average_normal(particles: &[ModelParticle], indices: &[usize]) -> Vec3 {
2437        if indices.is_empty() { return Vec3::Y; }
2438        let sum = indices.iter()
2439            .filter_map(|&i| particles.get(i))
2440            .fold(Vec3::ZERO, |a, p| a + p.normal);
2441        sum.normalize()
2442    }
2443
2444    /// Generate a color-coded particle overlay showing normals direction.
2445    pub fn normal_color_overlay(particles: &mut Vec<ModelParticle>) {
2446        for p in particles.iter_mut() {
2447            // Map normal components [-1,1] to [0,1] for RGB display
2448            let r = (p.normal.x * 0.5 + 0.5).clamp(0.0, 1.0);
2449            let g = (p.normal.y * 0.5 + 0.5).clamp(0.0, 1.0);
2450            let b = (p.normal.z * 0.5 + 0.5).clamp(0.0, 1.0);
2451            p.color = Vec4::new(r, g, b, 1.0);
2452        }
2453    }
2454
2455    /// Build a spatial hash grid for fast neighbor lookups.
2456    pub fn build_spatial_hash(
2457        particles: &[ModelParticle],
2458        cell_size: f32,
2459    ) -> HashMap<(i32, i32, i32), Vec<usize>> {
2460        let mut grid: HashMap<(i32, i32, i32), Vec<usize>> = HashMap::new();
2461        for (i, p) in particles.iter().enumerate() {
2462            let key = (
2463                (p.position.x / cell_size).floor() as i32,
2464                (p.position.y / cell_size).floor() as i32,
2465                (p.position.z / cell_size).floor() as i32,
2466            );
2467            grid.entry(key).or_default().push(i);
2468        }
2469        grid
2470    }
2471
2472    /// Query spatial hash grid for neighbors within radius.
2473    pub fn query_spatial_hash(
2474        grid: &HashMap<(i32, i32, i32), Vec<usize>>,
2475        center: Vec3,
2476        radius: f32,
2477        cell_size: f32,
2478    ) -> Vec<usize> {
2479        let r2 = radius * radius;
2480        let cx = (center.x / cell_size).floor() as i32;
2481        let cy = (center.y / cell_size).floor() as i32;
2482        let cz = (center.z / cell_size).floor() as i32;
2483        let cells = (radius / cell_size).ceil() as i32 + 1;
2484        let mut results = Vec::new();
2485        for dx in -cells..=cells {
2486            for dy in -cells..=cells {
2487                for dz in -cells..=cells {
2488                    if let Some(cell) = grid.get(&(cx + dx, cy + dy, cz + dz)) {
2489                        for &idx in cell { results.push(idx); }
2490                    }
2491                }
2492            }
2493        }
2494        // NOTE: caller must filter by actual distance
2495        results
2496    }
2497}
2498
2499// ============================================================
2500// MAIN MODEL EDITOR
2501// ============================================================
2502
2503#[derive(Clone, Debug)]
2504pub struct ModelEditor {
2505    pub models:           HashMap<u64, ParticleModel>,
2506    pub active_model_id:  Option<u64>,
2507    pub active_layer:     usize,
2508    pub active_brush:     BrushKind,
2509    pub brush_radius:     f32,
2510    pub brush_strength:   f32,
2511    pub brush_density:    f32,
2512    pub active_char:      char,
2513    pub active_color:     Vec4,
2514    pub selection:        HashSet<usize>,
2515    pub pivot:            Vec3,
2516    pub undo_stack:       VecDeque<ModelSnapshot>,
2517    pub redo_stack:       VecDeque<ModelSnapshot>,
2518    pub grid_snap:        bool,
2519    pub grid_size:        f32,
2520    pub symmetry:         SymmetryMode,
2521    pub next_model_id:    u64,
2522    pub brush_params:     BrushParams,
2523    pub selection_sys:    SelectionSystem,
2524    pub undo_sys:         UndoStack,
2525    pub proportional_edit: bool,
2526    pub proportional_radius: f32,
2527    pub proportional_falloff: FalloffCurve,
2528    pub wireframe_mode:   bool,
2529    pub normal_vis:       bool,
2530    pub show_bounds:      bool,
2531    pub lattice:          Option<LatticeDeformer>,
2532}
2533
2534impl ModelEditor {
2535    pub fn new() -> Self {
2536        Self {
2537            models:           HashMap::new(),
2538            active_model_id:  None,
2539            active_layer:     0,
2540            active_brush:     BrushKind::Add,
2541            brush_radius:     DEFAULT_BRUSH_RADIUS,
2542            brush_strength:   DEFAULT_BRUSH_STRENGTH,
2543            brush_density:    DEFAULT_BRUSH_DENSITY,
2544            active_char:      '.',
2545            active_color:     Vec4::ONE,
2546            selection:        HashSet::new(),
2547            pivot:            Vec3::ZERO,
2548            undo_stack:       VecDeque::new(),
2549            redo_stack:       VecDeque::new(),
2550            grid_snap:        false,
2551            grid_size:        0.25,
2552            symmetry:         SymmetryMode::None,
2553            next_model_id:    1,
2554            brush_params:     BrushParams::default(),
2555            selection_sys:    SelectionSystem::new(),
2556            undo_sys:         UndoStack::new(),
2557            proportional_edit: false,
2558            proportional_radius: 2.0,
2559            proportional_falloff: FalloffCurve::Smooth,
2560            wireframe_mode:   false,
2561            normal_vis:       false,
2562            show_bounds:      false,
2563            lattice:          None,
2564        }
2565    }
2566
2567    pub fn create_model(&mut self, name: impl Into<String>) -> u64 {
2568        let id = self.next_model_id;
2569        self.next_model_id += 1;
2570        let model = ParticleModel::new(id, name);
2571        self.models.insert(id, model);
2572        self.active_model_id = Some(id);
2573        id
2574    }
2575
2576    pub fn active_model(&self) -> Option<&ParticleModel> {
2577        self.active_model_id.and_then(|id| self.models.get(&id))
2578    }
2579
2580    pub fn active_model_mut(&mut self) -> Option<&mut ParticleModel> {
2581        self.active_model_id.and_then(|id| self.models.get_mut(&id))
2582    }
2583
2584    pub fn make_brush_params(&self) -> BrushParams {
2585        BrushParams {
2586            kind:      self.active_brush.clone(),
2587            radius:    self.brush_radius,
2588            strength:  self.brush_strength,
2589            density:   self.brush_density,
2590            color:     self.active_color,
2591            character: self.active_char,
2592            falloff:   FalloffCurve::Smooth,
2593        }
2594    }
2595
2596    fn push_undo(&mut self, label: &str) {
2597        if let Some(model) = self.active_model() {
2598            let snap = ModelSnapshot::capture(model, label);
2599            self.undo_sys.push(snap);
2600        }
2601    }
2602
2603    /// Apply brush at ray intersection point.
2604    pub fn apply_brush(&mut self, _ray: Ray3, hit_pos: Vec3) {
2605        let brush = self.active_brush.clone();
2606        let params = self.make_brush_params();
2607        let symmetry = self.symmetry.clone();
2608        let active_char = self.active_char;
2609        let active_color = self.active_color;
2610        self.push_undo(&format!("brush {:?}", brush));
2611
2612        if let Some(model) = self.active_model_mut() {
2613            match brush {
2614                BrushKind::Add => {
2615                    SculptEngine::apply_add(model, hit_pos, Vec3::Y, &params, active_char, active_color, &symmetry);
2616                }
2617                BrushKind::Remove => {
2618                    SculptEngine::apply_remove(model, hit_pos, &params, &symmetry);
2619                }
2620                BrushKind::Smooth => {
2621                    SculptEngine::apply_smooth(model, hit_pos, &params);
2622                }
2623                BrushKind::Inflate => {
2624                    SculptEngine::apply_inflate(model, hit_pos, &params);
2625                }
2626                BrushKind::Pinch => {
2627                    SculptEngine::apply_pinch(model, hit_pos, &params);
2628                }
2629                BrushKind::Color => {
2630                    SculptEngine::apply_color(model, hit_pos, &params);
2631                }
2632                BrushKind::Char => {
2633                    SculptEngine::apply_char(model, hit_pos, &params);
2634                }
2635                BrushKind::Flatten => {
2636                    SculptEngine::apply_flatten(model, hit_pos, &params);
2637                }
2638                BrushKind::Crease => {
2639                    SculptEngine::apply_crease(model, hit_pos, &params);
2640                }
2641                BrushKind::Clone => {
2642                    // Clone uses source/target; default to offset by up vector
2643                    let target = hit_pos + Vec3::new(params.radius * 2.0, 0.0, 0.0);
2644                    SculptEngine::apply_clone(model, hit_pos, target, &params);
2645                }
2646            }
2647            model.recompute_bounds();
2648        }
2649    }
2650
2651    // ---- TRANSFORM WRAPPERS ----
2652
2653    pub fn cmd_translate(&mut self, delta: Vec3) {
2654        self.push_undo("translate");
2655        let selection = self.selection.clone();
2656        let grid_snap = self.grid_snap;
2657        let grid_size = self.grid_size;
2658        let proportional = self.proportional_edit;
2659        if let Some(model) = self.active_model_mut() {
2660            if proportional {
2661                // handled separately
2662            } else {
2663                TransformTools::translate(&mut model.particles, &selection, delta);
2664            }
2665            if grid_snap {
2666                for &i in &selection {
2667                    if let Some(p) = model.particles.get_mut(i) {
2668                        p.position = p.snapped_position(grid_size);
2669                    }
2670                }
2671            }
2672            model.recompute_bounds();
2673        }
2674    }
2675
2676    pub fn cmd_rotate(&mut self, axis: Vec3, angle_radians: f32) {
2677        self.push_undo("rotate");
2678        let selection = self.selection.clone();
2679        let pivot = self.pivot;
2680        let quat = Quat::from_axis_angle(axis.normalize(), angle_radians);
2681        if let Some(model) = self.active_model_mut() {
2682            TransformTools::rotate(&mut model.particles, &selection, pivot, quat);
2683            model.recompute_bounds();
2684        }
2685    }
2686
2687    pub fn cmd_scale_uniform(&mut self, factor: f32) {
2688        self.push_undo("scale");
2689        let selection = self.selection.clone();
2690        let pivot = self.pivot;
2691        if let Some(model) = self.active_model_mut() {
2692            TransformTools::scale_uniform(&mut model.particles, &selection, pivot, factor);
2693            model.recompute_bounds();
2694        }
2695    }
2696
2697    pub fn cmd_scale_nonuniform(&mut self, factors: Vec3) {
2698        self.push_undo("scale_nonuniform");
2699        let selection = self.selection.clone();
2700        let pivot = self.pivot;
2701        if let Some(model) = self.active_model_mut() {
2702            TransformTools::scale_nonuniform(&mut model.particles, &selection, pivot, factors);
2703            model.recompute_bounds();
2704        }
2705    }
2706
2707    pub fn cmd_mirror(&mut self, axis: Vec3) {
2708        self.push_undo("mirror");
2709        let selection = self.selection.clone();
2710        let pivot = self.pivot;
2711        if let Some(model) = self.active_model_mut() {
2712            TransformTools::mirror(&mut model.particles, &selection, pivot, axis);
2713            model.recompute_bounds();
2714        }
2715    }
2716
2717    pub fn cmd_bend(&mut self, axis: Vec3, angle_per_unit: f32) {
2718        self.push_undo("bend");
2719        let selection = self.selection.clone();
2720        let pivot = self.pivot;
2721        if let Some(model) = self.active_model_mut() {
2722            TransformTools::bend(&mut model.particles, &selection, pivot, axis, angle_per_unit);
2723            model.recompute_bounds();
2724        }
2725    }
2726
2727    pub fn cmd_taper(&mut self, axis: Vec3, taper_factor: f32) {
2728        self.push_undo("taper");
2729        let selection = self.selection.clone();
2730        let pivot = self.pivot;
2731        if let Some(model) = self.active_model_mut() {
2732            TransformTools::taper(&mut model.particles, &selection, pivot, axis, taper_factor);
2733            model.recompute_bounds();
2734        }
2735    }
2736
2737    pub fn cmd_twist(&mut self, axis: Vec3, twist_rate: f32) {
2738        self.push_undo("twist");
2739        let selection = self.selection.clone();
2740        let pivot = self.pivot;
2741        if let Some(model) = self.active_model_mut() {
2742            TransformTools::twist(&mut model.particles, &selection, pivot, axis, twist_rate);
2743            model.recompute_bounds();
2744        }
2745    }
2746
2747    pub fn cmd_lattice_deform(&mut self) {
2748        if self.lattice.is_none() { return; }
2749        self.push_undo("lattice_deform");
2750        let selection = self.selection.clone();
2751        let lattice = self.lattice.clone().unwrap();
2752        if let Some(model) = self.active_model_mut() {
2753            TransformTools::lattice_deform(&mut model.particles, &selection, &lattice);
2754            model.recompute_bounds();
2755        }
2756    }
2757
2758    // ---- UNDO/REDO ----
2759
2760    pub fn undo(&mut self) {
2761        let id = self.active_model_id;
2762        if let Some(id) = id {
2763            if let Some(model) = self.models.get_mut(&id) {
2764                self.undo_sys.undo(model);
2765            }
2766        }
2767    }
2768
2769    pub fn redo(&mut self) {
2770        let id = self.active_model_id;
2771        if let Some(id) = id {
2772            if let Some(model) = self.models.get_mut(&id) {
2773                self.undo_sys.redo(model);
2774            }
2775        }
2776    }
2777
2778    // ---- SELECTION WRAPPERS ----
2779
2780    pub fn select_all(&mut self) {
2781        if let Some(model) = self.active_model() {
2782            let n = model.particles.len();
2783            self.selection = (0..n).collect();
2784        }
2785    }
2786
2787    pub fn deselect_all(&mut self) {
2788        self.selection.clear();
2789    }
2790
2791    pub fn invert_selection(&mut self) {
2792        if let Some(model) = self.active_model() {
2793            let n = model.particles.len();
2794            let all: HashSet<usize> = (0..n).collect();
2795            self.selection = all.difference(&self.selection).copied().collect();
2796        }
2797    }
2798
2799    pub fn box_select(&mut self, aabb: Aabb3, add: bool) {
2800        if let Some(model) = self.active_model() {
2801            let new_sel: HashSet<usize> = model.particles.iter().enumerate()
2802                .filter(|(_, p)| aabb.contains(p.position))
2803                .map(|(i, _)| i)
2804                .collect();
2805            if add { self.selection.extend(new_sel.iter()); }
2806            else   { self.selection = new_sel; }
2807        }
2808    }
2809
2810    pub fn sphere_select(&mut self, center: Vec3, radius: f32, add: bool) {
2811        if let Some(model) = self.active_model() {
2812            let r2 = radius * radius;
2813            let new_sel: HashSet<usize> = model.particles.iter().enumerate()
2814                .filter(|(_, p)| (p.position - center).length_squared() <= r2)
2815                .map(|(i, _)| i)
2816                .collect();
2817            if add { self.selection.extend(new_sel.iter()); }
2818            else   { self.selection = new_sel; }
2819        }
2820    }
2821
2822    pub fn select_by_char(&mut self, ch: char, add: bool) {
2823        if let Some(model) = self.active_model() {
2824            let new_sel: HashSet<usize> = model.particles.iter().enumerate()
2825                .filter(|(_, p)| p.character == ch)
2826                .map(|(i, _)| i)
2827                .collect();
2828            if add { self.selection.extend(new_sel.iter()); }
2829            else   { self.selection = new_sel; }
2830        }
2831    }
2832
2833    pub fn select_by_group(&mut self, group_id: u32, add: bool) {
2834        if let Some(model) = self.active_model() {
2835            let new_sel: HashSet<usize> = model.particles.iter().enumerate()
2836                .filter(|(_, p)| p.group_id == group_id)
2837                .map(|(i, _)| i)
2838                .collect();
2839            if add { self.selection.extend(new_sel.iter()); }
2840            else   { self.selection = new_sel; }
2841        }
2842    }
2843
2844    pub fn select_by_color(&mut self, target: Vec4, tolerance: f32, add: bool) {
2845        if let Some(model) = self.active_model() {
2846            let th = rgb_to_hsv(target.x, target.y, target.z);
2847            let new_sel: HashSet<usize> = model.particles.iter().enumerate()
2848                .filter(|(_, p)| {
2849                    let ph = rgb_to_hsv(p.color.x, p.color.y, p.color.z);
2850                    let d = ((hue_distance(th.0, ph.0)).powi(2)
2851                             + (th.1 - ph.1).powi(2)
2852                             + (th.2 - ph.2).powi(2)).sqrt();
2853                    d <= tolerance
2854                })
2855                .map(|(i, _)| i)
2856                .collect();
2857            if add { self.selection.extend(new_sel.iter()); }
2858            else   { self.selection = new_sel; }
2859        }
2860    }
2861
2862    pub fn grow_selection(&mut self, radius: f32) {
2863        if let Some(model) = self.active_model() {
2864            let r2 = radius * radius;
2865            let current: Vec<usize> = self.selection.iter().copied().collect();
2866            let n = model.particles.len();
2867            let mut additions = HashSet::new();
2868            for (i, p) in model.particles.iter().enumerate() {
2869                if self.selection.contains(&i) { continue; }
2870                for &sel in &current {
2871                    if (model.particles[sel].position - p.position).length_squared() <= r2 {
2872                        additions.insert(i);
2873                        break;
2874                    }
2875                }
2876            }
2877            self.selection.extend(additions.iter());
2878        }
2879    }
2880
2881    pub fn shrink_selection(&mut self, radius: f32) {
2882        if let Some(model) = self.active_model() {
2883            let r2 = radius * radius;
2884            let to_remove: HashSet<usize> = self.selection.iter().copied().filter(|&si| {
2885                model.particles.iter().enumerate().any(|(i, p)| {
2886                    !self.selection.contains(&i)
2887                        && (model.particles[si].position - p.position).length_squared() <= r2
2888                })
2889            }).collect();
2890            for idx in to_remove { self.selection.remove(&idx); }
2891        }
2892    }
2893
2894    // ---- PRIMITIVE INSERTION ----
2895
2896    pub fn insert_sphere(&mut self, center: Vec3, radius: f32, n: usize) {
2897        self.push_undo("insert_sphere");
2898        let ch = self.active_char;
2899        let col = self.active_color;
2900        let particles = PrimitiveBuilder::sphere(center, radius, n, ch, col);
2901        if let Some(model) = self.active_model_mut() {
2902            model.add_particles_bulk(particles);
2903        }
2904    }
2905
2906    pub fn insert_cube(&mut self, center: Vec3, half_size: Vec3, ppf: usize, fill: bool) {
2907        self.push_undo("insert_cube");
2908        let ch = self.active_char;
2909        let col = self.active_color;
2910        let particles = PrimitiveBuilder::cube(center, half_size, ppf, fill, ch, col);
2911        if let Some(model) = self.active_model_mut() {
2912            model.add_particles_bulk(particles);
2913        }
2914    }
2915
2916    pub fn insert_cylinder(&mut self, center: Vec3, radius: f32, height: f32, segs: usize, bands: usize) {
2917        self.push_undo("insert_cylinder");
2918        let ch = self.active_char;
2919        let col = self.active_color;
2920        let particles = PrimitiveBuilder::cylinder(center, radius, height, segs, bands, ch, col);
2921        if let Some(model) = self.active_model_mut() {
2922            model.add_particles_bulk(particles);
2923        }
2924    }
2925
2926    pub fn insert_torus(&mut self, center: Vec3, major: f32, minor: f32, us: usize, vs: usize) {
2927        self.push_undo("insert_torus");
2928        let ch = self.active_char;
2929        let col = self.active_color;
2930        let particles = PrimitiveBuilder::torus(center, major, minor, us, vs, ch, col);
2931        if let Some(model) = self.active_model_mut() {
2932            model.add_particles_bulk(particles);
2933        }
2934    }
2935
2936    pub fn insert_plane(&mut self, center: Vec3, w: f32, d: f32, cols: usize, rows: usize) {
2937        self.push_undo("insert_plane");
2938        let ch = self.active_char;
2939        let col = self.active_color;
2940        let particles = PrimitiveBuilder::plane(center, w, d, cols, rows, 0.5, 0.1, ch, col);
2941        if let Some(model) = self.active_model_mut() {
2942            model.add_particles_bulk(particles);
2943        }
2944    }
2945
2946    pub fn insert_text3d(&mut self, text: &str, origin: Vec3) {
2947        self.push_undo("insert_text3d");
2948        let ch = self.active_char;
2949        let col = self.active_color;
2950        let particles = PrimitiveBuilder::text3d(text, origin, 0.8, 1.0, 0.3, 5, ch, col);
2951        if let Some(model) = self.active_model_mut() {
2952            model.add_particles_bulk(particles);
2953        }
2954    }
2955
2956    pub fn insert_marching_cubes(
2957        &mut self,
2958        field: &dyn Fn(Vec3) -> f32,
2959        bounds: Aabb3,
2960        resolution: usize,
2961    ) {
2962        self.push_undo("insert_marching_cubes");
2963        let ch = self.active_char;
2964        let col = self.active_color;
2965        let particles = PrimitiveBuilder::marching_cubes(field, &bounds, resolution, MARCHING_CUBES_THRESHOLD, ch, col);
2966        if let Some(model) = self.active_model_mut() {
2967            model.add_particles_bulk(particles);
2968        }
2969    }
2970
2971    pub fn insert_metaballs(&mut self, balls: &[(Vec3, f32)], bounds: Aabb3, resolution: usize) {
2972        self.push_undo("insert_metaballs");
2973        let ch = self.active_char;
2974        let col = self.active_color;
2975        let particles = PrimitiveBuilder::metaballs(balls, &bounds, resolution, ch, col);
2976        if let Some(model) = self.active_model_mut() {
2977            model.add_particles_bulk(particles);
2978        }
2979    }
2980
2981    // ---- LOD ----
2982
2983    pub fn generate_lods(&mut self) {
2984        if let Some(model) = self.active_model_mut() {
2985            model.generate_lods();
2986        }
2987    }
2988
2989    // ---- NORMALS ----
2990
2991    pub fn recompute_normals(&mut self, k: usize) {
2992        if let Some(model) = self.active_model_mut() {
2993            VisualizationHelper::compute_normals_pca(&mut model.particles, k);
2994        }
2995    }
2996
2997    // ---- EXPORT ----
2998
2999    pub fn export_active_model(&self) -> Option<String> {
3000        self.active_model().map(ModelIO::export_text)
3001    }
3002
3003    pub fn import_model(&mut self, text: &str) -> Option<u64> {
3004        let id = self.next_model_id;
3005        self.next_model_id += 1;
3006        let model = ModelIO::import_text(text, id)?;
3007        self.models.insert(id, model);
3008        self.active_model_id = Some(id);
3009        Some(id)
3010    }
3011
3012    // ---- PICKING ----
3013
3014    pub fn pick(&self, ray: &Ray3, tolerance: f32) -> Option<(usize, f32)> {
3015        self.active_model().and_then(|m| {
3016            ParticlePicker::pick_closest(&m.particles, ray, tolerance)
3017        })
3018    }
3019
3020    // ---- LAYERS ----
3021
3022    pub fn add_layer(&mut self, name: impl Into<String>) {
3023        if let Some(model) = self.active_model_mut() {
3024            let id = model.add_layer(name);
3025            self.active_layer = id as usize;
3026        }
3027    }
3028
3029    pub fn set_active_layer(&mut self, layer_id: usize) {
3030        self.active_layer = layer_id;
3031    }
3032
3033    pub fn toggle_layer_visibility(&mut self, layer_id: u8) {
3034        if let Some(model) = self.active_model_mut() {
3035            if let Some(layer) = model.layers.iter_mut().find(|l| l.id == layer_id) {
3036                layer.toggle_visibility();
3037            }
3038        }
3039    }
3040
3041    pub fn merge_layers(&mut self, src: u8, dst: u8) {
3042        self.push_undo("merge_layers");
3043        if let Some(model) = self.active_model_mut() {
3044            model.merge_layer_into(src, dst);
3045        }
3046    }
3047
3048    // ---- PIVOT ----
3049
3050    pub fn set_pivot_to_selection_center(&mut self) {
3051        if let Some(model) = self.active_model() {
3052            if self.selection.is_empty() { return; }
3053            let sum: Vec3 = self.selection.iter()
3054                .filter_map(|&i| model.particles.get(i))
3055                .map(|p| p.position)
3056                .fold(Vec3::ZERO, |a, b| a + b);
3057            self.pivot = sum / self.selection.len() as f32;
3058        }
3059    }
3060
3061    pub fn set_pivot_to_model_center(&mut self) {
3062        if let Some(model) = self.active_model() {
3063            self.pivot = model.center_of_mass();
3064        }
3065    }
3066
3067    // ---- LATTICE SETUP ----
3068
3069    pub fn init_lattice(&mut self, res_x: usize, res_y: usize, res_z: usize) {
3070        if let Some(model) = self.active_model() {
3071            let bounds = model.bounds.clone();
3072            self.lattice = Some(LatticeDeformer::new(bounds, res_x, res_y, res_z));
3073        }
3074    }
3075
3076    pub fn set_lattice_control_point(&mut self, xi: usize, yi: usize, zi: usize, pos: Vec3) {
3077        if let Some(lattice) = &mut self.lattice {
3078            lattice.set_control_point(xi, yi, zi, pos);
3079        }
3080    }
3081
3082    pub fn reset_lattice(&mut self) {
3083        if let Some(lattice) = &mut self.lattice {
3084            lattice.reset();
3085        }
3086    }
3087
3088    // ---- SKELETON ----
3089
3090    pub fn add_bone(&mut self, name: impl Into<String>, head: Vec3, tail: Vec3) -> Option<u32> {
3091        if let Some(model) = self.active_model_mut() {
3092            let skel = model.skeleton.get_or_insert_with(ParticleSkeleton::new);
3093            let id = skel.bones.len() as u32;
3094            let bone = ParticleBone::new(id, name, head, tail);
3095            Some(skel.add_bone(bone))
3096        } else { None }
3097    }
3098
3099    pub fn bind_skeleton(&mut self) {
3100        if let Some(model) = self.active_model_mut() {
3101            if let Some(skel) = &mut model.skeleton {
3102                let mut ps = model.particles.clone();
3103                skel.bind_all_particles(&mut ps);
3104                model.particles = ps;
3105            }
3106        }
3107    }
3108
3109    pub fn apply_skeleton_pose(&mut self) {
3110        if let Some(model) = self.active_model_mut() {
3111            if let Some(skel) = &model.skeleton {
3112                for p in &mut model.particles {
3113                    p.position = skel.transform_position(p.position, &p.bone_indices, &p.bone_weights);
3114                }
3115            }
3116        }
3117    }
3118
3119    // ---- WIREFRAME / NORMALS VIS ----
3120
3121    pub fn get_wireframe_edges(&self) -> Vec<(usize, usize)> {
3122        self.active_model().map(|m| {
3123            VisualizationHelper::find_edges(&m.particles, self.brush_radius * 0.5)
3124        }).unwrap_or_default()
3125    }
3126
3127    pub fn get_normal_vis_particles(&self) -> Vec<ModelParticle> {
3128        self.active_model().map(|m| {
3129            VisualizationHelper::normal_visualization_particles(
3130                &m.particles, 0.15, '|', Vec4::new(0.0, 1.0, 0.5, 1.0)
3131            )
3132        }).unwrap_or_default()
3133    }
3134
3135    // ---- MISC ----
3136
3137    pub fn delete_selected(&mut self) {
3138        self.push_undo("delete");
3139        let sel = self.selection.clone();
3140        if let Some(model) = self.active_model_mut() {
3141            model.remove_particles(&sel);
3142        }
3143        self.selection.clear();
3144    }
3145
3146    pub fn duplicate_selected(&mut self) {
3147        self.push_undo("duplicate");
3148        let sel = self.selection.clone();
3149        if let Some(model) = self.active_model_mut() {
3150            let dups: Vec<ModelParticle> = sel.iter()
3151                .filter_map(|&i| model.particles.get(i))
3152                .cloned()
3153                .collect();
3154            let start = model.particles.len();
3155            model.add_particles_bulk(dups);
3156            // Select the duplicates
3157        }
3158    }
3159
3160    pub fn set_selected_group(&mut self, group_id: u32) {
3161        let sel: Vec<usize> = self.selection.iter().copied().collect();
3162        if let Some(model) = self.active_model_mut() {
3163            for i in sel {
3164                if let Some(p) = model.particles.get_mut(i) {
3165                    p.group_id = group_id;
3166                }
3167            }
3168        }
3169    }
3170
3171    pub fn set_selected_char(&mut self, ch: char) {
3172        let sel: Vec<usize> = self.selection.iter().copied().collect();
3173        if let Some(model) = self.active_model_mut() {
3174            for i in sel {
3175                if let Some(p) = model.particles.get_mut(i) {
3176                    p.character = ch;
3177                }
3178            }
3179        }
3180    }
3181
3182    pub fn set_selected_color(&mut self, color: Vec4) {
3183        let sel: Vec<usize> = self.selection.iter().copied().collect();
3184        if let Some(model) = self.active_model_mut() {
3185            for i in sel {
3186                if let Some(p) = model.particles.get_mut(i) {
3187                    p.color = color;
3188                }
3189            }
3190        }
3191    }
3192
3193    pub fn lock_selected(&mut self) {
3194        let sel: Vec<usize> = self.selection.iter().copied().collect();
3195        if let Some(model) = self.active_model_mut() {
3196            for i in sel {
3197                if let Some(p) = model.particles.get_mut(i) {
3198                    p.locked = true;
3199                }
3200            }
3201        }
3202    }
3203
3204    pub fn unlock_selected(&mut self) {
3205        let sel: Vec<usize> = self.selection.iter().copied().collect();
3206        if let Some(model) = self.active_model_mut() {
3207            for i in sel {
3208                if let Some(p) = model.particles.get_mut(i) {
3209                    p.locked = false;
3210                }
3211            }
3212        }
3213    }
3214
3215    pub fn center_model_at_origin(&mut self) {
3216        self.push_undo("center");
3217        if let Some(model) = self.active_model_mut() {
3218            let com = model.center_of_mass();
3219            for p in &mut model.particles {
3220                p.position -= com;
3221            }
3222            model.recompute_bounds();
3223        }
3224    }
3225
3226    pub fn flip_normals(&mut self) {
3227        let sel: Vec<usize> = self.selection.iter().copied().collect();
3228        if let Some(model) = self.active_model_mut() {
3229            for i in sel {
3230                if let Some(p) = model.particles.get_mut(i) {
3231                    p.normal = -p.normal;
3232                }
3233            }
3234        }
3235    }
3236
3237    pub fn particle_count(&self) -> usize {
3238        self.active_model().map(|m| m.particles.len()).unwrap_or(0)
3239    }
3240
3241    pub fn selected_count(&self) -> usize {
3242        self.selection.len()
3243    }
3244
3245    pub fn set_brush_radius(&mut self, r: f32) {
3246        self.brush_radius = r.max(EPSILON);
3247        self.brush_params.radius = self.brush_radius;
3248    }
3249
3250    pub fn set_brush_strength(&mut self, s: f32) {
3251        self.brush_strength = s.clamp(0.0, 1.0);
3252        self.brush_params.strength = self.brush_strength;
3253    }
3254
3255    pub fn set_brush_density(&mut self, d: f32) {
3256        self.brush_density = d.max(0.1);
3257        self.brush_params.density = self.brush_density;
3258    }
3259
3260    pub fn set_symmetry(&mut self, mode: SymmetryMode) {
3261        self.symmetry = mode;
3262    }
3263
3264    pub fn toggle_grid_snap(&mut self) {
3265        self.grid_snap = !self.grid_snap;
3266    }
3267}
3268
3269impl Default for ModelEditor {
3270    fn default() -> Self { Self::new() }
3271}
3272
3273// ============================================================
3274// ADDITIONAL MATH UTILITIES
3275// ============================================================
3276
3277/// Project a vector onto a plane defined by its normal.
3278pub fn project_onto_plane(v: Vec3, plane_normal: Vec3) -> Vec3 {
3279    let n = plane_normal.normalize();
3280    v - n * n.dot(v)
3281}
3282
3283/// Angle between two vectors in radians.
3284pub fn angle_between(a: Vec3, b: Vec3) -> f32 {
3285    let dot = a.normalize().dot(b.normalize()).clamp(-1.0, 1.0);
3286    dot.acos()
3287}
3288
3289/// Signed angle from a to b around axis.
3290pub fn signed_angle(a: Vec3, b: Vec3, axis: Vec3) -> f32 {
3291    let cross = a.cross(b);
3292    let sign = if cross.dot(axis) >= 0.0 { 1.0 } else { -1.0 };
3293    angle_between(a, b) * sign
3294}
3295
3296/// Linear interpolation between two colors in HSV space.
3297pub fn lerp_color_hsv(a: Vec4, b: Vec4, t: f32) -> Vec4 {
3298    let (ah, asat, av) = rgb_to_hsv(a.x, a.y, a.z);
3299    let (bh, bsat, bv) = rgb_to_hsv(b.x, b.y, b.z);
3300    let h = ah + (bh - ah) * t;
3301    let s = asat + (bsat - asat) * t;
3302    let v = av + (bv - av) * t;
3303    let (r, g, bl) = hsv_to_rgb(h, s, v);
3304    let alpha = a.w + (b.w - a.w) * t;
3305    Vec4::new(r, g, bl, alpha)
3306}
3307
3308/// Closest point on segment AB to point P.
3309pub fn closest_point_on_segment(a: Vec3, b: Vec3, p: Vec3) -> Vec3 {
3310    let ab = b - a;
3311    let len2 = ab.length_squared();
3312    if len2 < EPSILON { return a; }
3313    let t = ((p - a).dot(ab) / len2).clamp(0.0, 1.0);
3314    a + ab * t
3315}
3316
3317/// Barycentric coordinates of point P in triangle ABC.
3318pub fn barycentric(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
3319    let v0 = b - a;
3320    let v1 = c - a;
3321    let v2 = p - a;
3322    let d00 = v0.dot(v0);
3323    let d01 = v0.dot(v1);
3324    let d11 = v1.dot(v1);
3325    let d20 = v2.dot(v0);
3326    let d21 = v2.dot(v1);
3327    let denom = (d00 * d11 - d01 * d01).max(EPSILON);
3328    let v = (d11 * d20 - d01 * d21) / denom;
3329    let w = (d00 * d21 - d01 * d20) / denom;
3330    Vec3::new(1.0 - v - w, v, w)
3331}
3332
3333/// Clamp a Vec3 component-wise to given bounds.
3334pub fn clamp_vec3(v: Vec3, min: Vec3, max: Vec3) -> Vec3 {
3335    Vec3::new(
3336        v.x.clamp(min.x, max.x),
3337        v.y.clamp(min.y, max.y),
3338        v.z.clamp(min.z, max.z),
3339    )
3340}
3341
3342/// Remap a value from [in_min, in_max] to [out_min, out_max].
3343pub fn remap(val: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
3344    let t = (val - in_min) / (in_max - in_min + EPSILON);
3345    out_min + t * (out_max - out_min)
3346}
3347
3348/// Signed distance from point to AABB.
3349pub fn sdf_aabb(p: Vec3, half_extent: Vec3) -> f32 {
3350    let q = Vec3::new(p.x.abs(), p.y.abs(), p.z.abs()) - half_extent;
3351    q.max(Vec3::ZERO).length() + q.x.max(q.y).max(q.z).min(0.0)
3352}
3353
3354/// Signed distance from point to sphere.
3355pub fn sdf_sphere(p: Vec3, center: Vec3, radius: f32) -> f32 {
3356    (p - center).length() - radius
3357}
3358
3359/// Signed distance from point to torus.
3360pub fn sdf_torus(p: Vec3, major_r: f32, minor_r: f32) -> f32 {
3361    let q = Vec2::new((Vec2::new(p.x, p.z)).length() - major_r, p.y);
3362    q.length() - minor_r
3363}
3364
3365/// Signed distance from point to cylinder.
3366pub fn sdf_cylinder(p: Vec3, height: f32, radius: f32) -> f32 {
3367    let d = Vec2::new(Vec2::new(p.x, p.z).length() - radius, p.y.abs() - height * 0.5);
3368    d.x.max(d.y).min(0.0) + Vec2::new(d.x.max(0.0), d.y.max(0.0)).length()
3369}
3370
3371// ============================================================
3372// PARTICLE STATISTICS
3373// ============================================================
3374
3375#[derive(Clone, Debug, Default)]
3376pub struct ParticleStats {
3377    pub count:          usize,
3378    pub bounds:         Aabb3,
3379    pub center_of_mass: Vec3,
3380    pub avg_color:      Vec4,
3381    pub char_histogram: BTreeMap<char, usize>,
3382    pub group_counts:   BTreeMap<u32, usize>,
3383    pub avg_emission:   f32,
3384    pub surface_count:  usize,
3385    pub interior_count: usize,
3386}
3387
3388impl ParticleStats {
3389    pub fn compute(model: &ParticleModel) -> Self {
3390        let n = model.particles.len();
3391        if n == 0 { return Self::default(); }
3392
3393        let mut stats = Self::default();
3394        stats.count = n;
3395        stats.bounds = model.bounds.clone();
3396        stats.center_of_mass = model.center_of_mass();
3397
3398        let mut color_sum = Vec4::ZERO;
3399        let mut emission_sum = 0.0f32;
3400        for p in &model.particles {
3401            color_sum += p.color;
3402            emission_sum += p.emission;
3403            *stats.char_histogram.entry(p.character).or_insert(0) += 1;
3404            *stats.group_counts.entry(p.group_id).or_insert(0) += 1;
3405        }
3406        stats.avg_color    = color_sum / n as f32;
3407        stats.avg_emission = emission_sum / n as f32;
3408
3409        // Surface classification with default params
3410        let is_surface = VisualizationHelper::classify_surface_interior(&model.particles, 8, 0.5);
3411        stats.surface_count  = is_surface.iter().filter(|&&s| s).count();
3412        stats.interior_count = n - stats.surface_count;
3413
3414        stats
3415    }
3416}
3417
3418// ============================================================
3419// ADVANCED SCULPT OPERATIONS
3420// ============================================================
3421
3422pub struct AdvancedSculpt;
3423
3424impl AdvancedSculpt {
3425    /// Relax particles: iterative Laplacian smoothing over the whole model.
3426    pub fn global_relax(model: &mut ParticleModel, iterations: usize, strength: f32, neighbor_radius: f32) {
3427        let r2 = neighbor_radius * neighbor_radius;
3428        for _ in 0..iterations {
3429            let positions: Vec<Vec3> = model.particles.iter().map(|p| p.position).collect();
3430            for i in 0..model.particles.len() {
3431                if model.particles[i].locked { continue; }
3432                let pi = positions[i];
3433                let (sum, cnt) = positions.iter().enumerate()
3434                    .filter(|(j, q)| *j != i && (**q - pi).length_squared() <= r2)
3435                    .fold((Vec3::ZERO, 0usize), |(a, n), (_, q)| (a + *q, n + 1));
3436                if cnt > 0 {
3437                    let centroid = sum / cnt as f32;
3438                    model.particles[i].position = pi.lerp(centroid, strength);
3439                }
3440            }
3441        }
3442        model.recompute_bounds();
3443    }
3444
3445    /// Jitter: add random noise to particle positions.
3446    pub fn jitter(model: &mut ParticleModel, indices: &HashSet<usize>, amplitude: f32, seed: u32) {
3447        for (count, &i) in indices.iter().enumerate() {
3448            if let Some(p) = model.particles.get_mut(i) {
3449                if p.locked { continue; }
3450                let nx = (hash_2d(i as i32 * 3, seed as i32) * 2.0 - 1.0) * amplitude;
3451                let ny = (hash_2d(i as i32 * 7, seed as i32 + 1) * 2.0 - 1.0) * amplitude;
3452                let nz = (hash_2d(i as i32 * 13, seed as i32 + 2) * 2.0 - 1.0) * amplitude;
3453                p.position += Vec3::new(nx, ny, nz);
3454            }
3455        }
3456        model.recompute_bounds();
3457    }
3458
3459    /// Shrink-wrap: project each particle onto the surface of a target sphere.
3460    pub fn shrink_wrap_sphere(
3461        model: &mut ParticleModel,
3462        indices: &HashSet<usize>,
3463        center: Vec3,
3464        radius: f32,
3465        blend: f32,
3466    ) {
3467        for &i in indices {
3468            if let Some(p) = model.particles.get_mut(i) {
3469                if p.locked { continue; }
3470                let dir = (p.position - center).normalize();
3471                let target = center + dir * radius;
3472                p.position = p.position.lerp(target, blend);
3473            }
3474        }
3475        model.recompute_bounds();
3476    }
3477
3478    /// Push particles inside AABB to the surface.
3479    pub fn push_to_aabb_surface(
3480        model: &mut ParticleModel,
3481        indices: &HashSet<usize>,
3482        aabb: &Aabb3,
3483        blend: f32,
3484    ) {
3485        let center = aabb.center();
3486        let half = aabb.size() * 0.5;
3487        for &i in indices {
3488            if let Some(p) = model.particles.get_mut(i) {
3489                if p.locked { continue; }
3490                if !aabb.contains(p.position) { continue; }
3491                let local = p.position - center;
3492                // Find closest face
3493                let d = [
3494                    (half.x - local.x.abs(), Vec3::new(local.x.signum(), 0.0, 0.0)),
3495                    (half.y - local.y.abs(), Vec3::new(0.0, local.y.signum(), 0.0)),
3496                    (half.z - local.z.abs(), Vec3::new(0.0, 0.0, local.z.signum())),
3497                ];
3498                let (_, face_normal) = d.iter().min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)).copied().unwrap_or((0.0, Vec3::X));
3499                let face_pos = center + face_normal * half;
3500                let projected = p.position - face_normal * (face_normal.dot(p.position - face_pos));
3501                p.position = p.position.lerp(projected, blend);
3502            }
3503        }
3504        model.recompute_bounds();
3505    }
3506
3507    /// Array duplicate: create N copies of the selection along an axis.
3508    pub fn array_duplicate(
3509        model: &mut ParticleModel,
3510        indices: &HashSet<usize>,
3511        count: usize,
3512        step: Vec3,
3513    ) {
3514        let source: Vec<ModelParticle> = indices.iter()
3515            .filter_map(|&i| model.particles.get(i))
3516            .cloned()
3517            .collect();
3518        let mut new_particles = Vec::new();
3519        for step_i in 1..=count {
3520            let offset = step * step_i as f32;
3521            for p in &source {
3522                let mut np = p.clone();
3523                np.position += offset;
3524                new_particles.push(np);
3525            }
3526        }
3527        model.add_particles_bulk(new_particles);
3528    }
3529
3530    /// Radial array: place copies at evenly spaced angles around an axis.
3531    pub fn radial_array(
3532        model: &mut ParticleModel,
3533        indices: &HashSet<usize>,
3534        center: Vec3,
3535        axis: Vec3,
3536        count: usize,
3537    ) {
3538        let source: Vec<ModelParticle> = indices.iter()
3539            .filter_map(|&i| model.particles.get(i))
3540            .cloned()
3541            .collect();
3542        let mut new_particles = Vec::new();
3543        let axis_n = axis.normalize();
3544        for ci in 1..count {
3545            let angle = TAU * ci as f32 / count as f32;
3546            let quat = Quat::from_axis_angle(axis_n, angle);
3547            for p in &source {
3548                let local = p.position - center;
3549                let mut np = p.clone();
3550                np.position = center + quat * local;
3551                np.normal   = quat * p.normal;
3552                new_particles.push(np);
3553            }
3554        }
3555        model.add_particles_bulk(new_particles);
3556    }
3557
3558    /// Merge nearby particles within a threshold distance.
3559    pub fn merge_by_distance(model: &mut ParticleModel, threshold: f32) {
3560        let threshold2 = threshold * threshold;
3561        let n = model.particles.len();
3562        let mut merged = vec![false; n];
3563        let mut new_particles = Vec::new();
3564        for i in 0..n {
3565            if merged[i] { continue; }
3566            let pi = model.particles[i].position;
3567            let mut positions_to_merge = vec![pi];
3568            for j in (i+1)..n {
3569                if !merged[j] && (model.particles[j].position - pi).length_squared() <= threshold2 {
3570                    merged[j] = true;
3571                    positions_to_merge.push(model.particles[j].position);
3572                }
3573            }
3574            let avg = positions_to_merge.iter().fold(Vec3::ZERO, |a, &b| a + b) / positions_to_merge.len() as f32;
3575            let mut p = model.particles[i].clone();
3576            p.position = avg;
3577            new_particles.push(p);
3578        }
3579        model.particles = new_particles;
3580        model.recompute_bounds();
3581    }
3582
3583    /// Separate disconnected components into individual models.
3584    pub fn find_connected_components(
3585        model: &ParticleModel,
3586        max_edge_len: f32,
3587    ) -> Vec<Vec<usize>> {
3588        let n = model.particles.len();
3589        let max2 = max_edge_len * max_edge_len;
3590        let mut visited = vec![false; n];
3591        let mut components = Vec::new();
3592
3593        for start in 0..n {
3594            if visited[start] { continue; }
3595            let mut component = Vec::new();
3596            let mut queue = VecDeque::new();
3597            queue.push_back(start);
3598            visited[start] = true;
3599            while let Some(current) = queue.pop_front() {
3600                component.push(current);
3601                let pc = model.particles[current].position;
3602                for j in 0..n {
3603                    if !visited[j] && (model.particles[j].position - pc).length_squared() <= max2 {
3604                        visited[j] = true;
3605                        queue.push_back(j);
3606                    }
3607                }
3608            }
3609            components.push(component);
3610        }
3611        components
3612    }
3613
3614    /// Compute the convex hull center (centroid of bounding box corners).
3615    pub fn convex_hull_center(model: &ParticleModel) -> Vec3 {
3616        model.bounds.center()
3617    }
3618
3619    /// Scale model to fit in a unit cube.
3620    pub fn normalize_scale(model: &mut ParticleModel) {
3621        let size = model.bounds.size();
3622        let max_dim = size.x.max(size.y).max(size.z);
3623        if max_dim < EPSILON { return; }
3624        let scale = 1.0 / max_dim;
3625        let center = model.center_of_mass();
3626        for p in &mut model.particles {
3627            p.position = center + (p.position - center) * scale;
3628        }
3629        model.recompute_bounds();
3630    }
3631}
3632
3633// ============================================================
3634// PARTICLE EFFECTS
3635// ============================================================
3636
3637pub struct ParticleEffects;
3638
3639impl ParticleEffects {
3640    /// Animate particles: oscillate along their normal direction using a sine wave.
3641    pub fn animate_wave(
3642        model: &mut ParticleModel,
3643        time: f32,
3644        amplitude: f32,
3645        frequency: f32,
3646        phase_scale: f32,
3647    ) {
3648        for p in &mut model.particles {
3649            let phase = p.position.x * phase_scale + p.position.z * phase_scale;
3650            let offset = amplitude * (frequency * time + phase).sin();
3651            p.position += p.normal * offset;
3652        }
3653        model.recompute_bounds();
3654    }
3655
3656    /// Pulse emission over time.
3657    pub fn animate_emission_pulse(model: &mut ParticleModel, time: f32, frequency: f32) {
3658        for p in &mut model.particles {
3659            p.emission = 0.5 + 0.5 * (TAU * frequency * time).sin();
3660        }
3661    }
3662
3663    /// Attract particles toward a moving attractor point.
3664    pub fn attract(
3665        model: &mut ParticleModel,
3666        attractor: Vec3,
3667        strength: f32,
3668        radius: f32,
3669        delta_time: f32,
3670    ) {
3671        let r2 = radius * radius;
3672        for p in &mut model.particles {
3673            if p.locked { continue; }
3674            let d2 = (p.position - attractor).length_squared();
3675            if d2 > r2 || d2 < EPSILON { continue; }
3676            let dir = (attractor - p.position).normalize();
3677            let falloff = 1.0 - (d2 / r2).sqrt();
3678            p.position += dir * strength * falloff * delta_time;
3679        }
3680        model.recompute_bounds();
3681    }
3682
3683    /// Gravity simulation step.
3684    pub fn apply_gravity(
3685        model: &mut ParticleModel,
3686        gravity: Vec3,
3687        delta_time: f32,
3688        floor_y: Option<f32>,
3689    ) {
3690        for p in &mut model.particles {
3691            if p.locked { continue; }
3692            p.position += gravity * delta_time;
3693            if let Some(fy) = floor_y {
3694                if p.position.y < fy { p.position.y = fy; }
3695            }
3696        }
3697        model.recompute_bounds();
3698    }
3699
3700    /// Color cycle through hue over time.
3701    pub fn animate_color_cycle(model: &mut ParticleModel, time: f32, speed: f32) {
3702        for p in &mut model.particles {
3703            let (_, s, v) = rgb_to_hsv(p.color.x, p.color.y, p.color.z);
3704            let new_h = (time * speed * 360.0) % 360.0;
3705            let (r, g, b) = hsv_to_rgb(new_h, s.max(0.5), v.max(0.5));
3706            p.color = Vec4::new(r, g, b, p.color.w);
3707        }
3708    }
3709
3710    /// Scatter particles randomly within their bounding box.
3711    pub fn scatter(model: &mut ParticleModel, indices: &HashSet<usize>, seed: u32) {
3712        let bounds = model.bounds.clone();
3713        for &i in indices {
3714            if let Some(p) = model.particles.get_mut(i) {
3715                if p.locked { continue; }
3716                let rx = hash_2d(i as i32, seed as i32);
3717                let ry = hash_2d(i as i32 + 1000, seed as i32 + 1);
3718                let rz = hash_2d(i as i32 + 2000, seed as i32 + 2);
3719                p.position = bounds.min + bounds.size() * Vec3::new(rx, ry, rz);
3720            }
3721        }
3722        model.recompute_bounds();
3723    }
3724}
3725
3726// ============================================================
3727// GLYPH RENDERING HELPERS
3728// ============================================================
3729
3730/// For each particle, determine the best ASCII glyph for a given view direction.
3731pub fn compute_view_dependent_glyph(particle: &ModelParticle, view_dir: Vec3) -> char {
3732    let n = particle.normal;
3733    let dot = n.dot(-view_dir).clamp(-1.0, 1.0);
3734    // Select glyph by angle: facing = dense, grazing = sparse
3735    if dot > 0.8      { '#' }
3736    else if dot > 0.6 { '@' }
3737    else if dot > 0.4 { particle.character }
3738    else if dot > 0.2 { '.' }
3739    else if dot > 0.0 { ',' }
3740    else              { ' ' }
3741}
3742
3743/// Convert a depth value to ASCII shading character.
3744pub fn depth_to_ascii(depth: f32, near: f32, far: f32) -> char {
3745    const SHADING: &[char] = &[' ', '.', ':', ';', '-', '=', '+', '*', '#', '@', '█'];
3746    let t = 1.0 - ((depth - near) / (far - near)).clamp(0.0, 1.0);
3747    let idx = ((t * (SHADING.len() as f32 - 1.0)).round() as usize).min(SHADING.len() - 1);
3748    SHADING[idx]
3749}
3750
3751/// Sort particles back-to-front for correct alpha blending.
3752pub fn sort_particles_back_to_front(particles: &mut Vec<ModelParticle>, view_origin: Vec3) {
3753    particles.sort_by(|a, b| {
3754        let da = (a.position - view_origin).length_squared();
3755        let db = (b.position - view_origin).length_squared();
3756        db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
3757    });
3758}
3759
3760/// Sort particles front-to-back for depth testing.
3761pub fn sort_particles_front_to_back(particles: &mut Vec<ModelParticle>, view_origin: Vec3) {
3762    particles.sort_by(|a, b| {
3763        let da = (a.position - view_origin).length_squared();
3764        let db = (b.position - view_origin).length_squared();
3765        da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
3766    });
3767}
3768
3769/// Project a Vec3 world position to screen space (Vec2) given MVP matrix.
3770pub fn project_to_screen(pos: Vec3, mvp: Mat4, screen_w: f32, screen_h: f32) -> Option<Vec2> {
3771    let clip = mvp * Vec4::new(pos.x, pos.y, pos.z, 1.0);
3772    if clip.w.abs() < EPSILON { return None; }
3773    let ndc = Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
3774    if ndc.z < -1.0 || ndc.z > 1.0 { return None; }
3775    Some(Vec2::new(
3776        (ndc.x * 0.5 + 0.5) * screen_w,
3777        (1.0 - (ndc.y * 0.5 + 0.5)) * screen_h,
3778    ))
3779}
3780
3781/// Unproject screen position to a world-space Ray3.
3782pub fn unproject_ray(
3783    screen_x: f32,
3784    screen_y: f32,
3785    screen_w: f32,
3786    screen_h: f32,
3787    inv_mvp: Mat4,
3788) -> Ray3 {
3789    let ndc_x =  (screen_x / screen_w) * 2.0 - 1.0;
3790    let ndc_y = -((screen_y / screen_h) * 2.0 - 1.0);
3791    let near_clip = inv_mvp * Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
3792    let far_clip  = inv_mvp * Vec4::new(ndc_x, ndc_y,  1.0, 1.0);
3793    let near_world = Vec3::new(near_clip.x, near_clip.y, near_clip.z) / near_clip.w;
3794    let far_world  = Vec3::new(far_clip.x,  far_clip.y,  far_clip.z)  / far_clip.w;
3795    Ray3::new(near_world, (far_world - near_world).normalize())
3796}
3797
3798// ============================================================
3799// TESTS (compile-checked)
3800// ============================================================
3801
3802#[cfg(test)]
3803mod tests {
3804    use super::*;
3805
3806    #[test]
3807    fn test_aabb_expand() {
3808        let mut a = Aabb3::empty();
3809        a.expand(Vec3::new(1.0, 2.0, 3.0));
3810        a.expand(Vec3::new(-1.0, -2.0, -3.0));
3811        assert_eq!(a.min, Vec3::new(-1.0, -2.0, -3.0));
3812        assert_eq!(a.max, Vec3::new(1.0, 2.0, 3.0));
3813        assert!((a.center() - Vec3::ZERO).length() < EPSILON);
3814    }
3815
3816    #[test]
3817    fn test_ray_sphere() {
3818        let ray = Ray3::new(Vec3::new(0.0, 0.0, -5.0), Vec3::Z);
3819        let hit = ray.intersect_sphere(Vec3::ZERO, 1.0);
3820        assert!(hit.is_some());
3821        let t = hit.unwrap();
3822        assert!((t - 4.0).abs() < 1e-4, "t={}", t);
3823    }
3824
3825    #[test]
3826    fn test_smoothstep() {
3827        assert!((smoothstep(0.0, 1.0, 0.0) - 0.0).abs() < EPSILON);
3828        assert!((smoothstep(0.0, 1.0, 1.0) - 1.0).abs() < EPSILON);
3829        assert!((smoothstep(0.0, 1.0, 0.5) - 0.5).abs() < EPSILON);
3830    }
3831
3832    #[test]
3833    fn test_sphere_primitive() {
3834        let particles = PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 100, '.', Vec4::ONE);
3835        assert_eq!(particles.len(), 100);
3836        for p in &particles {
3837            let dist = p.position.length();
3838            assert!((dist - 1.0).abs() < 1e-4, "dist={}", dist);
3839        }
3840    }
3841
3842    #[test]
3843    fn test_torus_primitive() {
3844        let particles = PrimitiveBuilder::torus(Vec3::ZERO, 2.0, 0.5, 16, 8, '.', Vec4::ONE);
3845        assert_eq!(particles.len(), 16 * 8);
3846    }
3847
3848    #[test]
3849    fn test_rgb_hsv_roundtrip() {
3850        let (r, g, b) = (0.3, 0.6, 0.9);
3851        let (h, s, v) = rgb_to_hsv(r, g, b);
3852        let (r2, g2, b2) = hsv_to_rgb(h, s, v);
3853        assert!((r - r2).abs() < 1e-4);
3854        assert!((g - g2).abs() < 1e-4);
3855        assert!((b - b2).abs() < 1e-4);
3856    }
3857
3858    #[test]
3859    fn test_model_editor_create() {
3860        let mut editor = ModelEditor::new();
3861        let id = editor.create_model("test");
3862        assert_eq!(editor.active_model_id, Some(id));
3863        assert!(editor.active_model().is_some());
3864    }
3865
3866    #[test]
3867    fn test_insert_sphere_and_select() {
3868        let mut editor = ModelEditor::new();
3869        editor.create_model("m");
3870        editor.insert_sphere(Vec3::ZERO, 1.0, 50);
3871        assert_eq!(editor.particle_count(), 50);
3872        editor.sphere_select(Vec3::ZERO, 2.0, false);
3873        assert_eq!(editor.selected_count(), 50);
3874    }
3875
3876    #[test]
3877    fn test_undo_redo() {
3878        let mut editor = ModelEditor::new();
3879        editor.create_model("m");
3880        editor.insert_sphere(Vec3::ZERO, 1.0, 20);
3881        let before = editor.particle_count();
3882        editor.select_all();
3883        editor.delete_selected();
3884        assert_eq!(editor.particle_count(), 0);
3885        editor.undo();
3886        assert_eq!(editor.particle_count(), before);
3887    }
3888
3889    #[test]
3890    fn test_transform_translate() {
3891        let mut editor = ModelEditor::new();
3892        editor.create_model("m");
3893        editor.active_color = Vec4::ONE;
3894        editor.active_char = '.';
3895        if let Some(model) = editor.active_model_mut() {
3896            model.add_particle(ModelParticle::new(Vec3::ZERO, '.', Vec4::ONE));
3897        }
3898        editor.select_all();
3899        editor.cmd_translate(Vec3::new(1.0, 0.0, 0.0));
3900        if let Some(p) = editor.active_model().and_then(|m| m.particles.first()) {
3901            assert!((p.position.x - 1.0).abs() < EPSILON);
3902        }
3903    }
3904
3905    #[test]
3906    fn test_lattice_deformer_identity() {
3907        let bounds = Aabb3::new(-Vec3::ONE, Vec3::ONE);
3908        let lattice = LatticeDeformer::new(bounds, 2, 2, 2);
3909        let p = Vec3::new(0.5, 0.5, 0.5);
3910        let dp = lattice.deform(p);
3911        // Identity lattice should not move point
3912        assert!((dp - p).length() < 1e-3, "dp={:?}", dp);
3913    }
3914
3915    #[test]
3916    fn test_selection_grow_shrink() {
3917        let mut sel = SelectionSystem::new();
3918        let mut particles = vec![
3919            ModelParticle::new(Vec3::ZERO,            '.', Vec4::ONE),
3920            ModelParticle::new(Vec3::new(0.5, 0.0, 0.0), '.', Vec4::ONE),
3921            ModelParticle::new(Vec3::new(2.0, 0.0, 0.0), '.', Vec4::ONE),
3922        ];
3923        sel.add(0);
3924        sel.grow(&particles, 0.6);
3925        assert!(sel.selected.contains(&1));
3926        assert!(!sel.selected.contains(&2));
3927    }
3928
3929    #[test]
3930    fn test_export_import_roundtrip() {
3931        let mut model = ParticleModel::new(1, "test");
3932        model.add_particle(ModelParticle::new(Vec3::new(1.0, 2.0, 3.0), 'A', Vec4::new(1.0, 0.5, 0.0, 1.0)));
3933        let text = ModelIO::export_text(&model);
3934        let imported = ModelIO::import_text(&text, 2).unwrap();
3935        assert_eq!(imported.particles.len(), 1);
3936        assert_eq!(imported.particles[0].character, 'A');
3937        assert!((imported.particles[0].position - Vec3::new(1.0, 2.0, 3.0)).length() < 1e-4);
3938    }
3939
3940    #[test]
3941    fn test_barycentric() {
3942        let a = Vec3::ZERO;
3943        let b = Vec3::X;
3944        let c = Vec3::Y;
3945        let bc = barycentric(Vec3::new(0.5, 0.0, 0.0), a, b, c);
3946        assert!((bc.y - 0.5).abs() < 1e-4, "bc={:?}", bc);
3947    }
3948
3949    #[test]
3950    fn test_falloff_curves() {
3951        let curves = [
3952            FalloffCurve::Constant,
3953            FalloffCurve::Linear,
3954            FalloffCurve::Smooth,
3955            FalloffCurve::Sphere,
3956            FalloffCurve::Root,
3957            FalloffCurve::Sharp,
3958        ];
3959        for c in &curves {
3960            assert!((c.evaluate(0.0) - 1.0).abs() < EPSILON, "{:?} at 0", c);
3961            assert!((c.evaluate(1.0)).abs() < EPSILON || matches!(c, FalloffCurve::Constant),
3962                    "{:?} at 1 = {}", c, c.evaluate(1.0));
3963        }
3964    }
3965
3966    #[test]
3967    fn test_symmetry_mirrors() {
3968        let p = Vec3::new(1.0, 2.0, 3.0);
3969        let m = SymmetryMode::XYZ.mirrors(p);
3970        assert_eq!(m.len(), 7);
3971        assert!(m.contains(&Vec3::new(-1.0, 2.0, 3.0)));
3972        assert!(m.contains(&Vec3::new(-1.0, -2.0, -3.0)));
3973    }
3974
3975    #[test]
3976    fn test_sdf_sphere() {
3977        let p = Vec3::new(2.0, 0.0, 0.0);
3978        let d = sdf_sphere(p, Vec3::ZERO, 1.0);
3979        assert!((d - 1.0).abs() < EPSILON);
3980    }
3981
3982    #[test]
3983    fn test_particle_model_lod() {
3984        let mut model = ParticleModel::new(1, "lod");
3985        let particles = PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 200, '.', Vec4::ONE);
3986        model.add_particles_bulk(particles);
3987        model.generate_lods();
3988        assert_eq!(model.lod_levels.len(), 4);
3989        assert_eq!(model.lod_levels[0].particles.len(), 200);
3990        assert!(model.lod_levels[1].particles.len() <= 100);
3991    }
3992
3993    #[test]
3994    fn test_hash_2d() {
3995        // Should be deterministic
3996        assert_eq!(hash_2d(1, 2), hash_2d(1, 2));
3997        assert_ne!(hash_2d(1, 2), hash_2d(2, 1));
3998    }
3999
4000    #[test]
4001    fn test_spatial_hash() {
4002        let particles = PrimitiveBuilder::sphere(Vec3::ZERO, 2.0, 50, '.', Vec4::ONE);
4003        let grid = VisualizationHelper::build_spatial_hash(&particles, 1.0);
4004        let neighbors = VisualizationHelper::query_spatial_hash(&grid, Vec3::ZERO, 1.0, 1.0);
4005        assert!(!neighbors.is_empty());
4006    }
4007}
4008
4009// ============================================================
4010// SERIALIZATION HELPERS (no external deps)
4011// ============================================================
4012
4013pub struct ModelSerializer;
4014
4015impl ModelSerializer {
4016    /// Serialize a model to a compact binary-like text representation.
4017    pub fn serialize_compact(model: &ParticleModel) -> Vec<u8> {
4018        let mut out = Vec::new();
4019        // Header
4020        out.extend_from_slice(b"PMDL");
4021        out.extend_from_slice(&model.id.to_le_bytes());
4022        let name_bytes = model.name.as_bytes();
4023        out.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
4024        out.extend_from_slice(name_bytes);
4025        // Particle count
4026        out.extend_from_slice(&(model.particles.len() as u32).to_le_bytes());
4027        // Particles
4028        for p in &model.particles {
4029            out.extend_from_slice(&p.position.x.to_le_bytes());
4030            out.extend_from_slice(&p.position.y.to_le_bytes());
4031            out.extend_from_slice(&p.position.z.to_le_bytes());
4032            out.extend_from_slice(&(p.character as u32).to_le_bytes());
4033            out.extend_from_slice(&p.color.x.to_le_bytes());
4034            out.extend_from_slice(&p.color.y.to_le_bytes());
4035            out.extend_from_slice(&p.color.z.to_le_bytes());
4036            out.extend_from_slice(&p.color.w.to_le_bytes());
4037            out.extend_from_slice(&p.emission.to_le_bytes());
4038            out.extend_from_slice(&p.normal.x.to_le_bytes());
4039            out.extend_from_slice(&p.normal.y.to_le_bytes());
4040            out.extend_from_slice(&p.normal.z.to_le_bytes());
4041            out.push(p.group_id as u8);
4042            out.push(p.layer_id);
4043        }
4044        out
4045    }
4046
4047    /// Deserialize from compact format.
4048    pub fn deserialize_compact(data: &[u8]) -> Option<ParticleModel> {
4049        if data.len() < 16 { return None; }
4050        if &data[0..4] != b"PMDL" { return None; }
4051        let mut cursor = 4usize;
4052
4053        let id = u64::from_le_bytes(data[cursor..cursor+8].try_into().ok()?);
4054        cursor += 8;
4055        let name_len = u32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?) as usize;
4056        cursor += 4;
4057        if cursor + name_len > data.len() { return None; }
4058        let name = String::from_utf8(data[cursor..cursor+name_len].to_vec()).ok()?;
4059        cursor += name_len;
4060
4061        let particle_count = u32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?) as usize;
4062        cursor += 4;
4063
4064        let mut model = ParticleModel::new(id, name);
4065        let bytes_per_particle = 4*3 + 4 + 4*4 + 4 + 4*3 + 2; // 58 bytes
4066        if cursor + particle_count * bytes_per_particle > data.len() { return None; }
4067
4068        for _ in 0..particle_count {
4069            let px = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4070            let py = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4071            let pz = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4072            let ch_code = u32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4073            let cr = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4074            let cg = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4075            let cb = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4076            let ca = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4077            let emission = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4078            let nx = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4079            let ny = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4080            let nz = f32::from_le_bytes(data[cursor..cursor+4].try_into().ok()?); cursor += 4;
4081            let group_id = data[cursor] as u32; cursor += 1;
4082            let layer_id = data[cursor]; cursor += 1;
4083
4084            let ch = char::from_u32(ch_code).unwrap_or('.');
4085            let mut p = ModelParticle::new(
4086                Vec3::new(px, py, pz),
4087                ch,
4088                Vec4::new(cr, cg, cb, ca),
4089            );
4090            p.emission = emission;
4091            p.normal = Vec3::new(nx, ny, nz);
4092            p.group_id = group_id;
4093            p.layer_id = layer_id;
4094            model.add_particle(p);
4095        }
4096
4097        Some(model)
4098    }
4099}
4100
4101// ============================================================
4102// CLIPBOARD / COPY-PASTE SUPPORT
4103// ============================================================
4104
4105#[derive(Clone, Debug, Default)]
4106pub struct ModelClipboard {
4107    pub particles:       Vec<ModelParticle>,
4108    pub pivot:           Vec3,
4109    pub source_model_id: Option<u64>,
4110}
4111
4112impl ModelClipboard {
4113    pub fn new() -> Self { Self::default() }
4114
4115    pub fn copy_selection(&mut self, model: &ParticleModel, selection: &HashSet<usize>) {
4116        self.particles = selection.iter()
4117            .filter_map(|&i| model.particles.get(i))
4118            .cloned()
4119            .collect();
4120        self.pivot = if self.particles.is_empty() { Vec3::ZERO } else {
4121            let sum = self.particles.iter().map(|p| p.position).fold(Vec3::ZERO, |a, b| a + b);
4122            sum / self.particles.len() as f32
4123        };
4124        self.source_model_id = Some(model.id);
4125    }
4126
4127    pub fn paste_at(&self, model: &mut ParticleModel, target: Vec3) {
4128        let offset = target - self.pivot;
4129        let new_particles: Vec<ModelParticle> = self.particles.iter().map(|p| {
4130            let mut np = p.clone();
4131            np.position += offset;
4132            np
4133        }).collect();
4134        model.add_particles_bulk(new_particles);
4135    }
4136
4137    pub fn is_empty(&self) -> bool { self.particles.is_empty() }
4138
4139    pub fn count(&self) -> usize { self.particles.len() }
4140}
4141
4142// ============================================================
4143// GRID AND GUIDE HELPERS
4144// ============================================================
4145
4146pub struct GridHelper;
4147
4148impl GridHelper {
4149    /// Generate grid particle positions for a floor grid.
4150    pub fn floor_grid(
4151        center: Vec3,
4152        half_extent: f32,
4153        spacing: f32,
4154        y: f32,
4155        character: char,
4156        color: Vec4,
4157    ) -> Vec<ModelParticle> {
4158        let mut particles = Vec::new();
4159        let steps = (half_extent / spacing).ceil() as i32;
4160        for xi in -steps..=steps {
4161            for zi in -steps..=steps {
4162                let x = center.x + xi as f32 * spacing;
4163                let z = center.z + zi as f32 * spacing;
4164                particles.push(ModelParticle::new(Vec3::new(x, y, z), character, color).with_normal(Vec3::Y));
4165            }
4166        }
4167        particles
4168    }
4169
4170    /// Generate axis indicator particles.
4171    pub fn axis_indicators(length: f32, steps: usize) -> Vec<ModelParticle> {
4172        let mut particles = Vec::new();
4173        for i in 0..steps {
4174            let t = i as f32 / steps as f32 * length;
4175            particles.push(ModelParticle::new(Vec3::new(t, 0.0, 0.0), 'x', Vec4::new(1.0, 0.2, 0.2, 1.0)).with_normal(Vec3::X));
4176            particles.push(ModelParticle::new(Vec3::new(0.0, t, 0.0), 'y', Vec4::new(0.2, 1.0, 0.2, 1.0)).with_normal(Vec3::Y));
4177            particles.push(ModelParticle::new(Vec3::new(0.0, 0.0, t), 'z', Vec4::new(0.2, 0.2, 1.0, 1.0)).with_normal(Vec3::Z));
4178        }
4179        particles
4180    }
4181
4182    /// Snap a position to the nearest grid point.
4183    pub fn snap(pos: Vec3, grid_size: f32) -> Vec3 {
4184        if grid_size < EPSILON { return pos; }
4185        Vec3::new(
4186            (pos.x / grid_size).round() * grid_size,
4187            (pos.y / grid_size).round() * grid_size,
4188            (pos.z / grid_size).round() * grid_size,
4189        )
4190    }
4191
4192    /// Generate bounding box wireframe particles.
4193    pub fn bbox_wireframe(aabb: &Aabb3, density: usize, character: char, color: Vec4) -> Vec<ModelParticle> {
4194        let mut particles = Vec::new();
4195        let corners = [
4196            aabb.min,
4197            Vec3::new(aabb.max.x, aabb.min.y, aabb.min.z),
4198            Vec3::new(aabb.min.x, aabb.max.y, aabb.min.z),
4199            Vec3::new(aabb.max.x, aabb.max.y, aabb.min.z),
4200            Vec3::new(aabb.min.x, aabb.min.y, aabb.max.z),
4201            Vec3::new(aabb.max.x, aabb.min.y, aabb.max.z),
4202            Vec3::new(aabb.min.x, aabb.max.y, aabb.max.z),
4203            aabb.max,
4204        ];
4205        let edges: [(usize, usize); 12] = [
4206            (0,1),(2,3),(4,5),(6,7),
4207            (0,2),(1,3),(4,6),(5,7),
4208            (0,4),(1,5),(2,6),(3,7),
4209        ];
4210        for (a, b) in &edges {
4211            for i in 0..density {
4212                let t = i as f32 / (density as f32 - 1.0).max(1.0);
4213                let pos = corners[*a].lerp(corners[*b], t);
4214                particles.push(ModelParticle::new(pos, character, color));
4215            }
4216        }
4217        particles
4218    }
4219}
4220
4221// ============================================================
4222// CURVE PATH DEFORMER
4223// ============================================================
4224
4225#[derive(Clone, Debug)]
4226pub struct CatmullRomSpline {
4227    pub control_points: Vec<Vec3>,
4228    pub alpha:          f32,  // 0.0=uniform, 0.5=centripetal, 1.0=chordal
4229}
4230
4231impl CatmullRomSpline {
4232    pub fn new(alpha: f32) -> Self {
4233        Self { control_points: Vec::new(), alpha }
4234    }
4235
4236    pub fn add_point(&mut self, p: Vec3) {
4237        self.control_points.push(p);
4238    }
4239
4240    fn segment_t(p0: Vec3, p1: Vec3, alpha: f32) -> f32 {
4241        let d = (p1 - p0).length();
4242        d.powf(alpha)
4243    }
4244
4245    /// Evaluate spline at parameter t in [0, num_segments].
4246    pub fn evaluate(&self, t: f32) -> Vec3 {
4247        let n = self.control_points.len();
4248        if n < 2 { return self.control_points.first().copied().unwrap_or(Vec3::ZERO); }
4249        if n == 2 {
4250            return self.control_points[0].lerp(self.control_points[1], t.clamp(0.0, 1.0));
4251        }
4252
4253        let max_seg = (n - 1) as f32;
4254        let t = t.clamp(0.0, max_seg);
4255        let seg = (t as usize).min(n - 2);
4256        let local_t = t - seg as f32;
4257
4258        let p0 = self.control_points[seg.saturating_sub(1).max(0)];
4259        let p1 = self.control_points[seg];
4260        let p2 = self.control_points[(seg + 1).min(n - 1)];
4261        let p3 = self.control_points[(seg + 2).min(n - 1)];
4262
4263        // Catmull-Rom with alpha parameterization
4264        let t0 = 0.0f32;
4265        let t1 = t0 + Self::segment_t(p0, p1, self.alpha);
4266        let t2 = t1 + Self::segment_t(p1, p2, self.alpha);
4267        let t3 = t2 + Self::segment_t(p2, p3, self.alpha);
4268
4269        let t_param = t1 + local_t * (t2 - t1);
4270
4271        let safe_div = |a: Vec3, b: f32| -> Vec3 {
4272            if b.abs() < EPSILON { Vec3::ZERO } else { a / b }
4273        };
4274
4275        let a1 = safe_div(p0 * (t1 - t_param) + p1 * (t_param - t0), t1 - t0);
4276        let a2 = safe_div(p1 * (t2 - t_param) + p2 * (t_param - t1), t2 - t1);
4277        let a3 = safe_div(p2 * (t3 - t_param) + p3 * (t_param - t2), t3 - t2);
4278        let b1 = safe_div(a1 * (t2 - t_param) + a2 * (t_param - t0), t2 - t0);
4279        let b2 = safe_div(a2 * (t3 - t_param) + a3 * (t_param - t1), t3 - t1);
4280        safe_div(b1 * (t2 - t_param) + b2 * (t_param - t1), t2 - t1)
4281    }
4282
4283    /// Sample N evenly-spaced points along the spline.
4284    pub fn sample(&self, n: usize) -> Vec<Vec3> {
4285        if self.control_points.is_empty() { return Vec::new(); }
4286        let max_t = (self.control_points.len() - 1) as f32;
4287        (0..n).map(|i| {
4288            let t = i as f32 / (n as f32 - 1.0).max(1.0) * max_t;
4289            self.evaluate(t)
4290        }).collect()
4291    }
4292
4293    /// Deform particles to follow the spline.
4294    pub fn deform_along_path(
4295        &self,
4296        model: &mut ParticleModel,
4297        indices: &HashSet<usize>,
4298        axis: Vec3,
4299    ) {
4300        let axis_n = axis.normalize();
4301        let all_positions: Vec<Vec3> = indices.iter()
4302            .filter_map(|&i| model.particles.get(i))
4303            .map(|p| p.position)
4304            .collect();
4305        if all_positions.is_empty() { return; }
4306        let min_t = all_positions.iter().map(|&p| axis_n.dot(p)).fold(f32::MAX, f32::min);
4307        let max_t = all_positions.iter().map(|&p| axis_n.dot(p)).fold(f32::MIN, f32::max);
4308        let range = (max_t - min_t).max(EPSILON);
4309        let spline_len = (self.control_points.len() - 1) as f32;
4310
4311        for &i in indices {
4312            if let Some(p) = model.particles.get_mut(i) {
4313                if p.locked { continue; }
4314                let t = (axis_n.dot(p.position) - min_t) / range * spline_len;
4315                let spline_pos = self.evaluate(t);
4316                let perp = p.position - axis_n * axis_n.dot(p.position);
4317                p.position = spline_pos + perp;
4318            }
4319        }
4320        model.recompute_bounds();
4321    }
4322}
4323
4324// ============================================================
4325// BRUSH PRESET LIBRARY
4326// ============================================================
4327
4328#[derive(Clone, Debug)]
4329pub struct BrushPreset {
4330    pub name:     String,
4331    pub kind:     BrushKind,
4332    pub radius:   f32,
4333    pub strength: f32,
4334    pub density:  f32,
4335    pub falloff:  FalloffCurve,
4336    pub character: char,
4337    pub color:    Vec4,
4338}
4339
4340impl BrushPreset {
4341    pub fn new(name: impl Into<String>, kind: BrushKind) -> Self {
4342        Self {
4343            name:      name.into(),
4344            kind,
4345            radius:    1.0,
4346            strength:  0.5,
4347            density:   4.0,
4348            falloff:   FalloffCurve::Smooth,
4349            character: '.',
4350            color:     Vec4::ONE,
4351        }
4352    }
4353
4354    pub fn to_params(&self) -> BrushParams {
4355        BrushParams {
4356            kind:      self.kind.clone(),
4357            radius:    self.radius,
4358            strength:  self.strength,
4359            density:   self.density,
4360            color:     self.color,
4361            character: self.character,
4362            falloff:   self.falloff.clone(),
4363        }
4364    }
4365}
4366
4367#[derive(Clone, Debug, Default)]
4368pub struct BrushPresetLibrary {
4369    pub presets: BTreeMap<String, BrushPreset>,
4370}
4371
4372impl BrushPresetLibrary {
4373    pub fn new() -> Self {
4374        let mut lib = Self::default();
4375        lib.add_defaults();
4376        lib
4377    }
4378
4379    fn add_defaults(&mut self) {
4380        let mut add = BrushPreset::new("Default Add", BrushKind::Add);
4381        add.density = 8.0;
4382        self.presets.insert(add.name.clone(), add);
4383
4384        let mut smooth = BrushPreset::new("Heavy Smooth", BrushKind::Smooth);
4385        smooth.strength = 0.8;
4386        smooth.radius   = 2.0;
4387        self.presets.insert(smooth.name.clone(), smooth);
4388
4389        let mut inflate = BrushPreset::new("Inflate", BrushKind::Inflate);
4390        inflate.strength = 0.3;
4391        self.presets.insert(inflate.name.clone(), inflate);
4392
4393        let mut pinch = BrushPreset::new("Pinch", BrushKind::Pinch);
4394        pinch.radius = 0.5;
4395        pinch.strength = 0.7;
4396        self.presets.insert(pinch.name.clone(), pinch);
4397
4398        let flatten = BrushPreset::new("Flatten", BrushKind::Flatten);
4399        self.presets.insert(flatten.name.clone(), flatten);
4400    }
4401
4402    pub fn add(&mut self, preset: BrushPreset) {
4403        self.presets.insert(preset.name.clone(), preset);
4404    }
4405
4406    pub fn get(&self, name: &str) -> Option<&BrushPreset> {
4407        self.presets.get(name)
4408    }
4409
4410    pub fn names(&self) -> Vec<&str> {
4411        self.presets.keys().map(|s| s.as_str()).collect()
4412    }
4413}
4414
4415// ============================================================
4416// RENDERING CONTEXT HELPERS
4417// ============================================================
4418
4419/// Holds the data needed to render the model in the ASCII engine.
4420#[derive(Clone, Debug, Default)]
4421pub struct ModelRenderData {
4422    pub glyphs:      Vec<(Vec3, char, Vec4, f32)>,
4423    pub edge_pairs:  Vec<(Vec3, Vec3, char, Vec4)>,
4424    pub normal_tips: Vec<(Vec3, char, Vec4)>,
4425    pub bounds_wf:   Vec<(Vec3, char, Vec4)>,
4426    pub lod_level:   usize,
4427}
4428
4429impl ModelRenderData {
4430    pub fn build(
4431        editor: &ModelEditor,
4432        camera_pos:  Vec3,
4433        camera_dist: f32,
4434        lod_bias:    f32,
4435    ) -> Self {
4436        let mut data = Self::default();
4437        let model = match editor.active_model() { Some(m) => m, None => return data };
4438
4439        let lod_idx = model.select_lod(camera_dist, lod_bias);
4440        data.lod_level = lod_idx;
4441
4442        let particle_indices: &[usize] = if model.lod_levels.is_empty() {
4443            &[]
4444        } else {
4445            &model.lod_levels[lod_idx.min(model.lod_levels.len() - 1)].particles
4446        };
4447
4448        if particle_indices.is_empty() {
4449            // No LOD computed — render all
4450            for p in &model.particles {
4451                if let Some(layer) = model.layers.iter().find(|l| l.id == p.layer_id) {
4452                    if !layer.visible { continue; }
4453                    let alpha_blended = Vec4::new(p.color.x, p.color.y, p.color.z, p.color.w * layer.opacity);
4454                    data.glyphs.push((p.position, p.character, alpha_blended, p.emission));
4455                } else {
4456                    data.glyphs.push((p.position, p.character, p.color, p.emission));
4457                }
4458            }
4459        } else {
4460            for &pi in particle_indices {
4461                if let Some(p) = model.particles.get(pi) {
4462                    data.glyphs.push((p.position, p.character, p.color, p.emission));
4463                }
4464            }
4465        }
4466
4467        if editor.wireframe_mode {
4468            let edges = VisualizationHelper::find_edges(&model.particles, editor.brush_radius * 0.5);
4469            for (a, b) in edges {
4470                if let (Some(pa), Some(pb)) = (model.particles.get(a), model.particles.get(b)) {
4471                    data.edge_pairs.push((pa.position, pb.position, '-', Vec4::new(0.5, 0.5, 0.5, 1.0)));
4472                }
4473            }
4474        }
4475
4476        if editor.normal_vis {
4477            for p in &model.particles {
4478                let tip = p.position + p.normal * 0.2;
4479                data.normal_tips.push((tip, '^', Vec4::new(0.0, 1.0, 0.5, 1.0)));
4480            }
4481        }
4482
4483        if editor.show_bounds {
4484            let wf = GridHelper::bbox_wireframe(&model.bounds, 8, '+', Vec4::new(1.0, 1.0, 0.0, 0.8));
4485            for p in wf {
4486                data.bounds_wf.push((p.position, p.character, p.color));
4487            }
4488        }
4489
4490        data
4491    }
4492}
4493
4494// ============================================================
4495// TOOL CONTEXT (ties together editor + clipboard + presets)
4496// ============================================================
4497
4498pub struct ModelingToolContext {
4499    pub editor:    ModelEditor,
4500    pub clipboard: ModelClipboard,
4501    pub presets:   BrushPresetLibrary,
4502    pub spline:    CatmullRomSpline,
4503    pub stats:     Option<ParticleStats>,
4504}
4505
4506impl ModelingToolContext {
4507    pub fn new() -> Self {
4508        Self {
4509            editor:    ModelEditor::new(),
4510            clipboard: ModelClipboard::new(),
4511            presets:   BrushPresetLibrary::new(),
4512            spline:    CatmullRomSpline::new(0.5),
4513            stats:     None,
4514        }
4515    }
4516
4517    pub fn refresh_stats(&mut self) {
4518        self.stats = self.editor.active_model().map(ParticleStats::compute);
4519    }
4520
4521    pub fn apply_preset_brush(&mut self, preset_name: &str, hit_pos: Vec3, ray: Ray3) {
4522        if let Some(preset) = self.presets.get(preset_name) {
4523            let params = preset.to_params();
4524            self.editor.active_brush = params.kind.clone();
4525            self.editor.brush_radius  = params.radius;
4526            self.editor.brush_strength = params.strength;
4527            self.editor.brush_density  = params.density;
4528            self.editor.active_char    = params.character;
4529            self.editor.active_color   = params.color;
4530        }
4531        self.editor.apply_brush(ray, hit_pos);
4532    }
4533
4534    pub fn copy(&mut self) {
4535        let sel = self.editor.selection.clone();
4536        if let Some(model) = self.editor.active_model() {
4537            self.clipboard.copy_selection(model, &sel);
4538        }
4539    }
4540
4541    pub fn paste(&mut self, target: Vec3) {
4542        if self.clipboard.is_empty() { return; }
4543        self.editor.push_undo("paste");
4544        let clipboard = self.clipboard.clone();
4545        if let Some(model) = self.editor.active_model_mut() {
4546            clipboard.paste_at(model, target);
4547        }
4548    }
4549
4550    pub fn add_spline_point(&mut self, p: Vec3) {
4551        self.spline.add_point(p);
4552    }
4553
4554    pub fn apply_spline_deform(&mut self, axis: Vec3) {
4555        let sel = self.editor.selection.clone();
4556        let spline = self.spline.clone();
4557        if let Some(model) = self.editor.active_model_mut() {
4558            spline.deform_along_path(model, &sel, axis);
4559        }
4560    }
4561
4562    pub fn export(&self) -> Option<String> {
4563        self.editor.export_active_model()
4564    }
4565
4566    pub fn import(&mut self, text: &str) -> Option<u64> {
4567        self.editor.import_model(text)
4568    }
4569
4570    pub fn particle_count(&self) -> usize {
4571        self.editor.particle_count()
4572    }
4573
4574    pub fn selection_count(&self) -> usize {
4575        self.editor.selected_count()
4576    }
4577}
4578
4579impl Default for ModelingToolContext {
4580    fn default() -> Self { Self::new() }
4581}
4582
4583
4584
4585// ============================================================
4586// KD-TREE FOR FAST NEAREST NEIGHBOR SEARCH
4587// ============================================================
4588
4589#[derive(Clone, Debug)]
4590pub struct KdNode {
4591    pub position:     Vec3,
4592    pub particle_idx: usize,
4593    pub axis:         u8,
4594    pub left:         Option<Box<KdNode>>,
4595    pub right:        Option<Box<KdNode>>,
4596}
4597
4598impl KdNode {
4599    fn new(position: Vec3, particle_idx: usize, axis: u8) -> Self {
4600        Self { position, particle_idx, axis, left: None, right: None }
4601    }
4602}
4603
4604pub struct KdTree {
4605    pub root: Option<Box<KdNode>>,
4606    pub size: usize,
4607}
4608
4609impl KdTree {
4610    pub fn new() -> Self { Self { root: None, size: 0 } }
4611
4612    pub fn build(particles: &[ModelParticle]) -> Self {
4613        let mut indexed: Vec<(usize, Vec3)> = particles.iter().enumerate()
4614            .map(|(i, p)| (i, p.position)).collect();
4615        let root = Self::build_recursive(&mut indexed, 0);
4616        Self { root, size: particles.len() }
4617    }
4618
4619    fn build_recursive(points: &mut [(usize, Vec3)], depth: usize) -> Option<Box<KdNode>> {
4620        if points.is_empty() { return None; }
4621        let axis = (depth % 3) as u8;
4622        points.sort_by(|a, b| {
4623            let va = match axis { 0 => a.1.x, 1 => a.1.y, _ => a.1.z };
4624            let vb = match axis { 0 => b.1.x, 1 => b.1.y, _ => b.1.z };
4625            va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
4626        });
4627        let mid = points.len() / 2;
4628        let (idx, pos) = points[mid];
4629        let mut node = Box::new(KdNode::new(pos, idx, axis));
4630        node.left  = Self::build_recursive(&mut points[..mid], depth + 1);
4631        node.right = Self::build_recursive(&mut points[mid+1..], depth + 1);
4632        Some(node)
4633    }
4634
4635    pub fn k_nearest(&self, query: Vec3, k: usize) -> Vec<(usize, f32)> {
4636        let mut heap: Vec<(f32, usize)> = Vec::new();
4637        if let Some(root) = &self.root { Self::search_knn(root, query, k, &mut heap); }
4638        heap.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
4639        heap.into_iter().map(|(d, i)| (i, d.sqrt())).collect()
4640    }
4641
4642    fn search_knn(node: &KdNode, query: Vec3, k: usize, heap: &mut Vec<(f32, usize)>) {
4643        let dist_sq = (node.position - query).length_squared();
4644        if heap.len() < k {
4645            heap.push((dist_sq, node.particle_idx));
4646        } else {
4647            let worst = heap.iter().map(|(d, _)| *d).fold(0.0f32, f32::max);
4648            if dist_sq < worst {
4649                if let Some(pos) = heap.iter().position(|(d, _)| *d == worst) {
4650                    heap[pos] = (dist_sq, node.particle_idx);
4651                }
4652            }
4653        }
4654        let split_val = match node.axis {
4655            0 => query.x - node.position.x,
4656            1 => query.y - node.position.y,
4657            _ => query.z - node.position.z,
4658        };
4659        let (near, far) = if split_val <= 0.0 { (&node.left, &node.right) } else { (&node.right, &node.left) };
4660        if let Some(n) = near { Self::search_knn(n, query, k, heap); }
4661        let worst_dist = heap.iter().map(|(d, _)| *d).fold(0.0f32, f32::max);
4662        if heap.len() < k || split_val * split_val < worst_dist {
4663            if let Some(f) = far { Self::search_knn(f, query, k, heap); }
4664        }
4665    }
4666
4667    pub fn range_search(&self, query: Vec3, radius: f32) -> Vec<usize> {
4668        let mut results = Vec::new();
4669        if let Some(root) = &self.root { Self::search_range(root, query, radius * radius, &mut results); }
4670        results
4671    }
4672
4673    fn search_range(node: &KdNode, query: Vec3, radius_sq: f32, out: &mut Vec<usize>) {
4674        if (node.position - query).length_squared() <= radius_sq { out.push(node.particle_idx); }
4675        let split_dist = match node.axis {
4676            0 => query.x - node.position.x,
4677            1 => query.y - node.position.y,
4678            _ => query.z - node.position.z,
4679        };
4680        if let Some(left) = &node.left {
4681            if split_dist <= 0.0 || split_dist * split_dist <= radius_sq {
4682                Self::search_range(left, query, radius_sq, out);
4683            }
4684        }
4685        if let Some(right) = &node.right {
4686            if split_dist >= 0.0 || split_dist * split_dist <= radius_sq {
4687                Self::search_range(right, query, radius_sq, out);
4688            }
4689        }
4690    }
4691}
4692
4693impl Default for KdTree { fn default() -> Self { Self::new() } }
4694
4695// ============================================================
4696// PARTICLE MESH (surface topology)
4697// ============================================================
4698
4699#[derive(Clone, Debug, Default)]
4700pub struct ParticleMesh {
4701    pub vertices:      Vec<usize>,
4702    pub edges:         Vec<(usize, usize)>,
4703    pub faces:         Vec<[usize; 3]>,
4704    pub vert_to_faces: HashMap<usize, Vec<usize>>,
4705    pub vert_to_edges: HashMap<usize, Vec<usize>>,
4706}
4707
4708impl ParticleMesh {
4709    pub fn new() -> Self { Self::default() }
4710
4711    pub fn build_from_particles(particles: &[ModelParticle], max_edge_len: f32) -> Self {
4712        let mut mesh = Self::new();
4713        let n = particles.len();
4714        mesh.vertices = (0..n).collect();
4715        let max2 = max_edge_len * max_edge_len;
4716        for i in 0..n {
4717            for j in (i+1)..n {
4718                if (particles[i].position - particles[j].position).length_squared() <= max2 {
4719                    let eid = mesh.edges.len();
4720                    mesh.edges.push((i, j));
4721                    mesh.vert_to_edges.entry(i).or_default().push(eid);
4722                    mesh.vert_to_edges.entry(j).or_default().push(eid);
4723                }
4724            }
4725        }
4726        for eid1 in 0..mesh.edges.len() {
4727            let (a, b) = mesh.edges[eid1];
4728            let a_n: HashSet<usize> = mesh.vert_to_edges.get(&a)
4729                .map(|eids| eids.iter().map(|&e| { let (ea, eb) = mesh.edges[e]; if ea == a { eb } else { ea } }).collect())
4730                .unwrap_or_default();
4731            let b_n: HashSet<usize> = mesh.vert_to_edges.get(&b)
4732                .map(|eids| eids.iter().map(|&e| { let (ea, eb) = mesh.edges[e]; if ea == b { eb } else { ea } }).collect())
4733                .unwrap_or_default();
4734            for &c in a_n.intersection(&b_n) {
4735                let mut tri = [a, b, c];
4736                tri.sort_unstable();
4737                if !mesh.faces.iter().any(|f| f == &tri) {
4738                    let fid = mesh.faces.len();
4739                    mesh.faces.push(tri);
4740                    for &v in &tri { mesh.vert_to_faces.entry(v).or_default().push(fid); }
4741                }
4742            }
4743        }
4744        mesh
4745    }
4746
4747    pub fn face_normal(&self, face_idx: usize, particles: &[ModelParticle]) -> Vec3 {
4748        let [a, b, c] = self.faces[face_idx];
4749        (particles[b].position - particles[a].position)
4750            .cross(particles[c].position - particles[a].position).normalize()
4751    }
4752
4753    pub fn vertex_normal(&self, vert_idx: usize, particles: &[ModelParticle]) -> Vec3 {
4754        match self.vert_to_faces.get(&vert_idx) {
4755            None => Vec3::Y,
4756            Some(fids) => {
4757                let sum = fids.iter().map(|&fi| self.face_normal(fi, particles)).fold(Vec3::ZERO, |a, b| a + b);
4758                sum.normalize()
4759            }
4760        }
4761    }
4762
4763    pub fn laplacian_smooth_step(&self, particles: &mut Vec<ModelParticle>, strength: f32) {
4764        let positions: Vec<Vec3> = particles.iter().map(|p| p.position).collect();
4765        for &vi in &self.vertices {
4766            if particles[vi].locked { continue; }
4767            if let Some(eids) = self.vert_to_edges.get(&vi) {
4768                let mut sum = Vec3::ZERO; let mut cnt = 0usize;
4769                for &eid in eids {
4770                    let (a, b) = self.edges[eid];
4771                    sum += positions[if a == vi { b } else { a }]; cnt += 1;
4772                }
4773                if cnt > 0 { particles[vi].position = positions[vi].lerp(sum / cnt as f32, strength); }
4774            }
4775        }
4776    }
4777
4778    pub fn average_edge_length(&self, particles: &[ModelParticle]) -> f32 {
4779        if self.edges.is_empty() { return 0.0; }
4780        self.edges.iter().map(|&(a, b)| (particles[a].position - particles[b].position).length())
4781            .sum::<f32>() / self.edges.len() as f32
4782    }
4783
4784    pub fn boundary_vertices(&self) -> Vec<usize> {
4785        self.vertices.iter().copied().filter(|&v| {
4786            self.vert_to_edges.get(&v).map(|eids| eids.iter().any(|&eid| {
4787                let (a, b) = self.edges[eid];
4788                self.faces.iter().filter(|f| f.contains(&a) && f.contains(&b)).count() == 1
4789            })).unwrap_or(false)
4790        }).collect()
4791    }
4792}
4793
4794// ============================================================
4795// REMESHING
4796// ============================================================
4797
4798pub struct Remesher;
4799
4800impl Remesher {
4801    pub fn resample_poisson(model: &mut ParticleModel, target_density: f32, character: char, color: Vec4) {
4802        let bounds = model.bounds.clone();
4803        let size = bounds.size();
4804        let target_count = (size.x * size.y * size.z * target_density) as usize;
4805        if target_count == 0 { return; }
4806        let existing: Vec<Vec3> = model.particles.iter().map(|p| p.position).collect();
4807        if existing.is_empty() { return; }
4808        let min_dist = (1.0 / target_density.max(EPSILON)).cbrt();
4809        let min_dist2 = min_dist * min_dist;
4810        let mut placed: Vec<Vec3> = Vec::new();
4811        let mut candidates: VecDeque<Vec3> = existing.iter().cloned().collect();
4812        let mut attempts = 0usize;
4813        while let Some(candidate) = candidates.pop_front() {
4814            if attempts > target_count * 10 { break; }
4815            attempts += 1;
4816            if placed.iter().all(|&q| (candidate - q).length_squared() >= min_dist2) {
4817                placed.push(candidate);
4818                if placed.len() >= target_count { break; }
4819                for k in 0i32..4 {
4820                    let offset = Vec3::new(
4821                        (hash_2d(placed.len() as i32 * 3 + k, attempts as i32) * 2.0 - 1.0) * min_dist * 2.0,
4822                        (hash_2d(placed.len() as i32 * 7 + k, attempts as i32 + 1) * 2.0 - 1.0) * min_dist * 2.0,
4823                        (hash_2d(placed.len() as i32 * 11 + k, attempts as i32 + 2) * 2.0 - 1.0) * min_dist * 2.0,
4824                    );
4825                    let nc = candidate + offset;
4826                    if bounds.contains(nc) { candidates.push_back(nc); }
4827                }
4828            }
4829        }
4830        model.particles.clear();
4831        model.layers.iter_mut().for_each(|l| l.particle_indices.clear());
4832        model.add_particles_bulk(placed.into_iter().map(|p| ModelParticle::new(p, character, color)).collect());
4833    }
4834
4835    pub fn adaptive_subdivide(model: &mut ParticleModel, max_edge_len: f32) {
4836        let existing: Vec<ModelParticle> = model.particles.clone();
4837        let max2 = max_edge_len * max_edge_len;
4838        let mut new_mids: Vec<ModelParticle> = Vec::new();
4839        for i in 0..existing.len() {
4840            for j in (i+1)..existing.len() {
4841                let d2 = (existing[i].position - existing[j].position).length_squared();
4842                if d2 > max2 && d2 < max2 * 4.0 {
4843                    let mid_pos = (existing[i].position + existing[j].position) * 0.5;
4844                    let mut mp = ModelParticle::new(mid_pos, existing[i].character, existing[i].color.lerp(existing[j].color, 0.5));
4845                    mp.normal = (existing[i].normal + existing[j].normal).normalize();
4846                    new_mids.push(mp);
4847                }
4848            }
4849        }
4850        model.add_particles_bulk(new_mids);
4851    }
4852
4853    pub fn decimate(model: &mut ParticleModel, min_dist: f32) {
4854        let n = model.particles.len();
4855        let min2 = min_dist * min_dist;
4856        let mut keep = vec![true; n];
4857        for i in 0..n {
4858            if !keep[i] { continue; }
4859            for j in (i+1)..n {
4860                if keep[j] && (model.particles[i].position - model.particles[j].position).length_squared() < min2 {
4861                    keep[j] = false;
4862                }
4863            }
4864        }
4865        let to_remove: HashSet<usize> = keep.iter().enumerate().filter(|(_, &k)| !k).map(|(i, _)| i).collect();
4866        model.remove_particles(&to_remove);
4867    }
4868
4869    pub fn isotropic_remesh(model: &mut ParticleModel, target_edge_len: f32, iterations: usize) {
4870        for _ in 0..iterations {
4871            Self::adaptive_subdivide(model, target_edge_len * 1.5);
4872            Self::decimate(model, target_edge_len * 0.5);
4873            AdvancedSculpt::global_relax(model, 2, 0.3, target_edge_len * 2.0);
4874        }
4875    }
4876}
4877
4878// ============================================================
4879// 3D NOISE GENERATORS
4880// ============================================================
4881
4882fn hash_3d(x: i32, y: i32, z: i32) -> f32 {
4883    let n = x.wrapping_mul(1619).wrapping_add(y.wrapping_mul(31337))
4884             .wrapping_add(z.wrapping_mul(6271)).wrapping_add(1013904223);
4885    let n = n.wrapping_mul(1664525).wrapping_add(1013904223);
4886    ((n as u32) as f32) / (u32::MAX as f32)
4887}
4888
4889pub struct NoiseGenerator;
4890
4891impl NoiseGenerator {
4892    pub fn value_3d(x: f32, y: f32, z: f32) -> f32 {
4893        let (xi, yi, zi) = (x.floor() as i32, y.floor() as i32, z.floor() as i32);
4894        let (xf, yf, zf) = (smoothstep(0.0, 1.0, x - xi as f32), smoothstep(0.0, 1.0, y - yi as f32), smoothstep(0.0, 1.0, z - zi as f32));
4895        let c000 = hash_3d(xi,   yi,   zi  ); let c100 = hash_3d(xi+1, yi,   zi  );
4896        let c010 = hash_3d(xi,   yi+1, zi  ); let c110 = hash_3d(xi+1, yi+1, zi  );
4897        let c001 = hash_3d(xi,   yi,   zi+1); let c101 = hash_3d(xi+1, yi,   zi+1);
4898        let c011 = hash_3d(xi,   yi+1, zi+1); let c111 = hash_3d(xi+1, yi+1, zi+1);
4899        let x00 = c000 + xf * (c100 - c000); let x10 = c010 + xf * (c110 - c010);
4900        let x01 = c001 + xf * (c101 - c001); let x11 = c011 + xf * (c111 - c011);
4901        let y0 = x00 + yf * (x10 - x00); let y1 = x01 + yf * (x11 - x01);
4902        y0 + zf * (y1 - y0)
4903    }
4904
4905    pub fn fbm(x: f32, y: f32, z: f32, octaves: usize, lacunarity: f32, gain: f32) -> f32 {
4906        let (mut value, mut amp, mut freq) = (0.0f32, 0.5f32, 1.0f32);
4907        for _ in 0..octaves {
4908            value += amp * (Self::value_3d(x * freq, y * freq, z * freq) * 2.0 - 1.0);
4909            freq *= lacunarity; amp *= gain;
4910        }
4911        value * 0.5 + 0.5
4912    }
4913
4914    pub fn turbulence(x: f32, y: f32, z: f32, octaves: usize) -> f32 {
4915        let (mut value, mut amp, mut freq) = (0.0f32, 0.5f32, 1.0f32);
4916        for _ in 0..octaves {
4917            value += amp * (Self::value_3d(x * freq, y * freq, z * freq) * 2.0 - 1.0).abs();
4918            freq *= 2.0; amp *= 0.5;
4919        }
4920        value
4921    }
4922
4923    pub fn displace_fbm(model: &mut ParticleModel, indices: &HashSet<usize>, scale: f32, amplitude: f32, octaves: usize) {
4924        for &i in indices {
4925            if let Some(p) = model.particles.get_mut(i) {
4926                if p.locked { continue; }
4927                let n = Self::fbm(p.position.x * scale, p.position.y * scale, p.position.z * scale, octaves, 2.0, 0.5);
4928                p.position += p.normal * (n * 2.0 - 1.0) * amplitude;
4929            }
4930        }
4931        model.recompute_bounds();
4932    }
4933
4934    pub fn domain_warp(x: f32, y: f32, z: f32, ws: f32) -> f32 {
4935        let wx = Self::fbm(x + 1.7, y + 9.2, z + 5.5, 4, 2.0, 0.5);
4936        let wy = Self::fbm(x + 8.3, y + 2.8, z + 1.2, 4, 2.0, 0.5);
4937        let wz = Self::fbm(x + 3.1, y + 6.4, z + 7.8, 4, 2.0, 0.5);
4938        Self::fbm(x + ws * wx, y + ws * wy, z + ws * wz, 4, 2.0, 0.5)
4939    }
4940}
4941
4942// ============================================================
4943// PARTICLE MATERIAL SYSTEM
4944// ============================================================
4945
4946#[derive(Clone, Debug)]
4947pub enum MaterialType { Flat, Emissive, Metallic, Subsurface, Toon, Hologram }
4948
4949#[derive(Clone, Debug)]
4950pub struct ParticleMaterial {
4951    pub name:       String,
4952    pub mat_type:   MaterialType,
4953    pub base_color: Vec4,
4954    pub emission:   f32,
4955    pub roughness:  f32,
4956    pub char_set:   Vec<char>,
4957}
4958
4959impl ParticleMaterial {
4960    pub fn new(name: impl Into<String>, mat_type: MaterialType) -> Self {
4961        Self { name: name.into(), mat_type, base_color: Vec4::ONE, emission: 0.0, roughness: 0.5,
4962               char_set: vec![' ', '.', ':', ';', '+', '*', '#', '@'] }
4963    }
4964
4965    pub fn shade(&self, normal: Vec3, light_dir: Vec3, view_dir: Vec3) -> Vec4 {
4966        let (n, l, v) = (normal.normalize(), light_dir.normalize(), view_dir.normalize());
4967        let h = (l + v).normalize();
4968        match self.mat_type {
4969            MaterialType::Flat      => self.base_color,
4970            MaterialType::Emissive  => self.base_color * (1.0 + self.emission),
4971            MaterialType::Metallic  => {
4972                let nd = n.dot(l).max(0.0);
4973                let sp = n.dot(h).max(0.0).powf(1.0 / (self.roughness * self.roughness + EPSILON));
4974                Vec4::new(self.base_color.x * nd + sp, self.base_color.y * nd + sp, self.base_color.z * nd + sp, 1.0)
4975            }
4976            MaterialType::Subsurface => { let w = (n.dot(l) + 0.5) / 1.5; self.base_color * w }
4977            MaterialType::Toon       => {
4978                let nd = n.dot(l);
4979                let t = if nd > 0.8 { 1.0 } else if nd > 0.3 { 0.6 } else { 0.2 };
4980                self.base_color * t
4981            }
4982            MaterialType::Hologram   => {
4983                let f = (1.0 - n.dot(v).abs()).powi(3);
4984                Vec4::new(self.base_color.x * f, self.base_color.y * f, self.base_color.z * f, f)
4985            }
4986        }
4987    }
4988
4989    pub fn glyph_for_shade(&self, shade: f32) -> char {
4990        if self.char_set.is_empty() { return '.'; }
4991        let idx = ((shade.clamp(0.0, 1.0) * (self.char_set.len() as f32 - 1.0)).round() as usize).min(self.char_set.len() - 1);
4992        self.char_set[idx]
4993    }
4994
4995    pub fn apply_to_particle(&self, p: &mut ModelParticle, light_dir: Vec3, view_dir: Vec3) {
4996        let shaded = self.shade(p.normal, light_dir, view_dir);
4997        p.color = shaded; p.emission = self.emission;
4998        p.character = self.glyph_for_shade((shaded.x + shaded.y + shaded.z) / 3.0);
4999    }
5000}
5001
5002#[derive(Clone, Debug, Default)]
5003pub struct MaterialLibrary { pub materials: HashMap<String, ParticleMaterial> }
5004
5005impl MaterialLibrary {
5006    pub fn new() -> Self { let mut l = Self::default(); l.add_defaults(); l }
5007
5008    fn add_defaults(&mut self) {
5009        self.materials.insert("default".into(), ParticleMaterial::new("default", MaterialType::Flat));
5010        self.materials.insert("metal".into(),   ParticleMaterial::new("metal",   MaterialType::Metallic));
5011        let mut e = ParticleMaterial::new("emit", MaterialType::Emissive); e.emission = 2.0;
5012        self.materials.insert("emit".into(), e);
5013        self.materials.insert("toon".into(), ParticleMaterial::new("toon", MaterialType::Toon));
5014        self.materials.insert("holo".into(), ParticleMaterial::new("holo", MaterialType::Hologram));
5015    }
5016
5017    pub fn add(&mut self, mat: ParticleMaterial) { self.materials.insert(mat.name.clone(), mat); }
5018    pub fn get(&self, name: &str) -> Option<&ParticleMaterial> { self.materials.get(name) }
5019
5020    pub fn apply_to_model(&self, model: &mut ParticleModel, name: &str, light: Vec3, view: Vec3) {
5021        if let Some(mat) = self.get(name) {
5022            let mat = mat.clone();
5023            for p in &mut model.particles { mat.apply_to_particle(p, light, view); }
5024        }
5025    }
5026}
5027
5028// ============================================================
5029// SCULPT MASK
5030// ============================================================
5031
5032#[derive(Clone, Debug)]
5033pub struct SculptMask { pub values: Vec<f32>, pub count: usize }
5034
5035impl SculptMask {
5036    pub fn new(count: usize) -> Self { Self { values: vec![1.0; count], count } }
5037
5038    pub fn from_selection(sel: &HashSet<usize>, total: usize) -> Self {
5039        let mut m = Self::new(total);
5040        for i in 0..total { m.values[i] = if sel.contains(&i) { 1.0 } else { 0.0 }; }
5041        m
5042    }
5043
5044    pub fn invert(&mut self) { for v in &mut self.values { *v = 1.0 - *v; } }
5045    pub fn fill(&mut self, value: f32) { for v in &mut self.values { *v = value.clamp(0.0, 1.0); } }
5046    pub fn paint(&mut self, idx: usize, value: f32) { if let Some(v) = self.values.get_mut(idx) { *v = value.clamp(0.0, 1.0); } }
5047
5048    pub fn blur(&mut self, particles: &[ModelParticle], radius: f32, iters: usize) {
5049        let r2 = radius * radius;
5050        for _ in 0..iters {
5051            let prev = self.values.clone();
5052            for i in 0..self.count {
5053                let pi = particles.get(i).map(|p| p.position).unwrap_or(Vec3::ZERO);
5054                let (mut sum, mut cnt) = (0.0f32, 0usize);
5055                for (j, &v) in prev.iter().enumerate() {
5056                    if let Some(pj) = particles.get(j) {
5057                        if (pj.position - pi).length_squared() <= r2 { sum += v; cnt += 1; }
5058                    }
5059                }
5060                if cnt > 0 { self.values[i] = sum / cnt as f32; }
5061            }
5062        }
5063    }
5064
5065    pub fn to_selection(&self, threshold: f32) -> HashSet<usize> {
5066        self.values.iter().enumerate().filter(|(_, &v)| v >= threshold).map(|(i, _)| i).collect()
5067    }
5068
5069    pub fn combine_multiply(&mut self, other: &SculptMask) {
5070        for (a, &b) in self.values.iter_mut().zip(other.values.iter()) { *a *= b; }
5071    }
5072}
5073
5074// ============================================================
5075// PARTICLE DELTA
5076// ============================================================
5077
5078#[derive(Clone, Debug, Default)]
5079pub struct ParticleDelta {
5080    pub modified: Vec<(usize, Vec3, Vec3)>,
5081    pub added:    Vec<ModelParticle>,
5082    pub removed:  Vec<(usize, ModelParticle)>,
5083}
5084
5085impl ParticleDelta {
5086    pub fn new() -> Self { Self::default() }
5087
5088    pub fn compute(before: &[ModelParticle], after: &[ModelParticle]) -> Self {
5089        let mut d = Self::new();
5090        let min_len = before.len().min(after.len());
5091        for i in 0..min_len {
5092            if (before[i].position - after[i].position).length_squared() > EPSILON * EPSILON {
5093                d.modified.push((i, before[i].position, after[i].position));
5094            }
5095        }
5096        if after.len() > before.len() { for i in before.len()..after.len() { d.added.push(after[i].clone()); } }
5097        else if before.len() > after.len() { for i in after.len()..before.len() { d.removed.push((i, before[i].clone())); } }
5098        d
5099    }
5100
5101    pub fn is_empty(&self) -> bool { self.modified.is_empty() && self.added.is_empty() && self.removed.is_empty() }
5102    pub fn memory_estimate(&self) -> usize { self.modified.len() * 28 + self.added.len() * std::mem::size_of::<ModelParticle>() }
5103}
5104
5105// ============================================================
5106// STENCIL
5107// ============================================================
5108
5109#[derive(Clone, Debug)]
5110pub struct Stencil { pub name: String, pub width: usize, pub height: usize, pub data: Vec<f32> }
5111
5112impl Stencil {
5113    pub fn new(name: impl Into<String>, w: usize, h: usize) -> Self {
5114        Self { name: name.into(), width: w, height: h, data: vec![0.0; w * h] }
5115    }
5116
5117    pub fn set_pixel(&mut self, x: usize, y: usize, v: f32) {
5118        if x < self.width && y < self.height { self.data[y * self.width + x] = v.clamp(0.0, 1.0); }
5119    }
5120
5121    pub fn get_pixel(&self, x: usize, y: usize) -> f32 {
5122        if x < self.width && y < self.height { self.data[y * self.width + x] } else { 0.0 }
5123    }
5124
5125    pub fn sample(&self, u: f32, v: f32) -> f32 {
5126        let x = u * (self.width as f32 - 1.0);  let y = v * (self.height as f32 - 1.0);
5127        let xi = x.floor() as usize; let yi = y.floor() as usize;
5128        let xt = x - xi as f32; let yt = y - yi as f32;
5129        let xi2 = (xi + 1).min(self.width - 1); let yi2 = (yi + 1).min(self.height - 1);
5130        let c00 = self.get_pixel(xi, yi); let c10 = self.get_pixel(xi2, yi);
5131        let c01 = self.get_pixel(xi, yi2); let c11 = self.get_pixel(xi2, yi2);
5132        (c00 + xt * (c10 - c00)) + yt * ((c01 + xt * (c11 - c01)) - (c00 + xt * (c10 - c00)))
5133    }
5134
5135    pub fn circle(name: impl Into<String>, res: usize) -> Self {
5136        let mut s = Self::new(name, res, res);
5137        let c = res as f32 / 2.0;
5138        for y in 0..res {
5139            for x in 0..res {
5140                let dx = x as f32 - c; let dy = y as f32 - c;
5141                s.set_pixel(x, y, smoothstep(0.0, 1.0, (1.0 - (dx*dx + dy*dy).sqrt() / c.max(EPSILON)).clamp(0.0, 1.0)));
5142            }
5143        }
5144        s
5145    }
5146}
5147
5148// ============================================================
5149// GROUP MANAGER
5150// ============================================================
5151
5152#[derive(Clone, Debug)]
5153pub struct GroupInfo { pub id: u32, pub name: String, pub visible: bool, pub locked: bool, pub color: Vec4 }
5154
5155impl GroupInfo {
5156    pub fn new(id: u32, name: impl Into<String>, color: Vec4) -> Self {
5157        Self { id, name: name.into(), visible: true, locked: false, color }
5158    }
5159}
5160
5161#[derive(Clone, Debug, Default)]
5162pub struct GroupManager { pub groups: BTreeMap<u32, GroupInfo>, pub next_id: u32 }
5163
5164impl GroupManager {
5165    pub fn new() -> Self { Self::default() }
5166
5167    pub fn create_group(&mut self, name: impl Into<String>, color: Vec4) -> u32 {
5168        let id = self.next_id; self.next_id += 1;
5169        self.groups.insert(id, GroupInfo::new(id, name, color)); id
5170    }
5171
5172    pub fn get_group(&self, id: u32) -> Option<&GroupInfo> { self.groups.get(&id) }
5173    pub fn set_visibility(&mut self, id: u32, v: bool) { if let Some(g) = self.groups.get_mut(&id) { g.visible = v; } }
5174    pub fn set_lock(&mut self, id: u32, l: bool) { if let Some(g) = self.groups.get_mut(&id) { g.locked = l; } }
5175
5176    pub fn visible_groups(&self) -> Vec<u32> {
5177        self.groups.values().filter(|g| g.visible).map(|g| g.id).collect()
5178    }
5179
5180    pub fn is_particle_active(&self, p: &ModelParticle) -> bool {
5181        self.groups.get(&p.group_id).map(|g| g.visible && !g.locked).unwrap_or(true)
5182    }
5183}
5184
5185// ============================================================
5186// PROCEDURAL PATTERNS
5187// ============================================================
5188
5189pub struct ProceduralPatterns;
5190
5191impl ProceduralPatterns {
5192    pub fn voronoi(model: &mut ParticleModel, seeds: &[Vec3], colors: &[Vec4]) {
5193        if seeds.is_empty() { return; }
5194        for p in &mut model.particles {
5195            let (best, _) = seeds.iter().enumerate()
5196                .map(|(i, &s)| (i, (p.position - s).length_squared()))
5197                .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
5198                .unwrap_or((0, 0.0));
5199            p.color = colors.get(best).copied().unwrap_or(Vec4::ONE);
5200        }
5201    }
5202
5203    pub fn stripes(model: &mut ParticleModel, axis: Vec3, width: f32, ca: Vec4, cb: Vec4) {
5204        let n = axis.normalize();
5205        for p in &mut model.particles {
5206            p.color = if (n.dot(p.position) / width.max(EPSILON)).floor() as i32 % 2 == 0 { ca } else { cb };
5207        }
5208    }
5209
5210    pub fn checkerboard(model: &mut ParticleModel, cell: f32, ca: Vec4, cb: Vec4) {
5211        for p in &mut model.particles {
5212            let xi = (p.position.x / cell.max(EPSILON)).floor() as i32;
5213            let yi = (p.position.y / cell.max(EPSILON)).floor() as i32;
5214            let zi = (p.position.z / cell.max(EPSILON)).floor() as i32;
5215            p.color = if (xi + yi + zi) % 2 == 0 { ca } else { cb };
5216        }
5217    }
5218
5219    pub fn gradient_along_axis(model: &mut ParticleModel, axis: Vec3, c0: Vec4, c1: Vec4) {
5220        let n = axis.normalize();
5221        let ts: Vec<f32> = model.particles.iter().map(|p| n.dot(p.position)).collect();
5222        let min_t = ts.iter().cloned().fold(f32::MAX, f32::min);
5223        let max_t = ts.iter().cloned().fold(f32::MIN, f32::max);
5224        let range = (max_t - min_t).max(EPSILON);
5225        for (i, p) in model.particles.iter_mut().enumerate() { p.color = c0.lerp(c1, (ts[i] - min_t) / range); }
5226    }
5227
5228    pub fn radial_gradient(model: &mut ParticleModel, center: Vec3, radius: f32, cc: Vec4, ce: Vec4) {
5229        for p in &mut model.particles {
5230            p.color = cc.lerp(ce, ((p.position - center).length() / radius.max(EPSILON)).clamp(0.0, 1.0));
5231        }
5232    }
5233
5234    pub fn reaction_diffusion_step(
5235        u: &mut Vec<f32>, v: &mut Vec<f32>, particles: &[ModelParticle],
5236        du: f32, dv: f32, feed: f32, kill: f32, dt: f32, radius: f32,
5237    ) {
5238        let n = particles.len(); let r2 = radius * radius;
5239        let u0 = u.clone(); let v0 = v.clone();
5240        for i in 0..n {
5241            let pi = particles[i].position; let ui = u0[i]; let vi = v0[i];
5242            let (mut lu, mut lv, mut cnt) = (0.0f32, 0.0f32, 0usize);
5243            for (j, (&uj, &vj)) in u0.iter().zip(v0.iter()).enumerate() {
5244                if j != i && (particles[j].position - pi).length_squared() <= r2 {
5245                    lu += uj - ui; lv += vj - vi; cnt += 1;
5246                }
5247            }
5248            if cnt > 0 { lu /= cnt as f32; lv /= cnt as f32; }
5249            let uvv = ui * vi * vi;
5250            u[i] = (ui + (du * lu - uvv + feed * (1.0 - ui)) * dt).clamp(0.0, 1.0);
5251            v[i] = (vi + (dv * lv + uvv - (kill + feed) * vi) * dt).clamp(0.0, 1.0);
5252        }
5253    }
5254
5255    pub fn apply_rd_color(model: &mut ParticleModel, u: &[f32], v: &[f32], ca: Vec4, cb: Vec4) {
5256        for (i, p) in model.particles.iter_mut().enumerate() {
5257            let ui = u.get(i).copied().unwrap_or(1.0);
5258            let vi = v.get(i).copied().unwrap_or(0.0);
5259            p.color = ca.lerp(cb, ((ui - vi + 1.0) * 0.5).clamp(0.0, 1.0));
5260        }
5261    }
5262}
5263
5264// ============================================================
5265// EASING + MODEL ANIMATION
5266// ============================================================
5267
5268#[derive(Clone, Debug, PartialEq)]
5269pub enum EasingType { Linear, EaseIn, EaseOut, EaseInOut, Bounce, Elastic }
5270
5271impl EasingType {
5272    pub fn apply(&self, t: f32) -> f32 {
5273        let t = t.clamp(0.0, 1.0);
5274        match self {
5275            EasingType::Linear    => t,
5276            EasingType::EaseIn    => t * t,
5277            EasingType::EaseOut   => 1.0 - (1.0 - t) * (1.0 - t),
5278            EasingType::EaseInOut => smoothstep(0.0, 1.0, t),
5279            EasingType::Bounce    => {
5280                let t2 = 1.0 - t; let n = 7.5625f32; let d = 2.75f32;
5281                let v = if t2 < 1.0/d { n*t2*t2 }
5282                    else if t2 < 2.0/d { let t3 = t2 - 1.5/d; n*t3*t3 + 0.75 }
5283                    else if t2 < 2.5/d { let t3 = t2 - 2.25/d; n*t3*t3 + 0.9375 }
5284                    else { let t3 = t2 - 2.625/d; n*t3*t3 + 0.984375 };
5285                1.0 - v
5286            }
5287            EasingType::Elastic   => {
5288                if t == 0.0 || t == 1.0 { t }
5289                else { -(2.0f32.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * TAU / 3.0).sin() }
5290            }
5291        }
5292    }
5293}
5294
5295#[derive(Clone, Debug)]
5296pub struct ModelKeyframe { pub time: f32, pub snapshot: ModelSnapshot, pub easing: EasingType }
5297
5298#[derive(Clone, Debug, Default)]
5299pub struct ModelAnimation { pub name: String, pub keyframes: Vec<ModelKeyframe>, pub duration: f32, pub looping: bool }
5300
5301impl ModelAnimation {
5302    pub fn new(name: impl Into<String>) -> Self { Self { name: name.into(), ..Default::default() } }
5303
5304    pub fn add_keyframe(&mut self, time: f32, snapshot: ModelSnapshot, easing: EasingType) {
5305        let kf = ModelKeyframe { time, snapshot, easing };
5306        let pos = self.keyframes.partition_point(|k| k.time < time);
5307        self.keyframes.insert(pos, kf);
5308        self.duration = self.keyframes.last().map(|k| k.time).unwrap_or(0.0);
5309    }
5310
5311    pub fn evaluate(&self, time: f32, model: &mut ParticleModel) {
5312        if self.keyframes.is_empty() { return; }
5313        let time = if self.looping { time % self.duration.max(EPSILON) } else { time.min(self.duration) };
5314        let idx = self.keyframes.partition_point(|k| k.time <= time);
5315        if idx == 0 { self.keyframes[0].snapshot.restore_to(model); return; }
5316        if idx >= self.keyframes.len() { self.keyframes.last().unwrap().snapshot.restore_to(model); return; }
5317        let prev = &self.keyframes[idx - 1]; let next = &self.keyframes[idx];
5318        let t = next.easing.apply((time - prev.time) / (next.time - prev.time).max(EPSILON));
5319        let len = prev.snapshot.particles.len().min(next.snapshot.particles.len()).min(model.particles.len());
5320        for i in 0..len {
5321            model.particles[i].position = prev.snapshot.particles[i].position.lerp(next.snapshot.particles[i].position, t);
5322            model.particles[i].color    = prev.snapshot.particles[i].color.lerp(next.snapshot.particles[i].color, t);
5323        }
5324        model.recompute_bounds();
5325    }
5326}
5327
5328// ============================================================
5329// EXTRA MATH HELPERS
5330// ============================================================
5331
5332pub fn decompose_mat4(m: Mat4) -> (Vec3, Quat, Vec3) {
5333    let translation = Vec3::new(m.w_axis.x, m.w_axis.y, m.w_axis.z);
5334    let sx = Vec3::new(m.x_axis.x, m.x_axis.y, m.x_axis.z).length();
5335    let sy = Vec3::new(m.y_axis.x, m.y_axis.y, m.y_axis.z).length();
5336    let sz = Vec3::new(m.z_axis.x, m.z_axis.y, m.z_axis.z).length();
5337    let rot = Mat4::from_cols(m.x_axis / sx, m.y_axis / sy, m.z_axis / sz, Vec4::new(0.0, 0.0, 0.0, 1.0));
5338    (translation, Quat::from_mat4(&rot), Vec3::new(sx, sy, sz))
5339}
5340
5341pub fn euler_to_quat(roll: f32, pitch: f32, yaw: f32) -> Quat {
5342    Quat::from_euler(glam::EulerRot::XYZ, roll, pitch, yaw)
5343}
5344
5345pub fn quat_to_euler(q: Quat) -> (f32, f32, f32) { q.to_euler(glam::EulerRot::XYZ) }
5346
5347pub fn bounding_sphere(points: &[Vec3]) -> (Vec3, f32) {
5348    if points.is_empty() { return (Vec3::ZERO, 0.0); }
5349    let min_x = points.iter().min_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)).copied().unwrap_or(Vec3::ZERO);
5350    let max_x = points.iter().max_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)).copied().unwrap_or(Vec3::ZERO);
5351    let mut center = (min_x + max_x) * 0.5; let mut radius = (max_x - min_x).length() * 0.5;
5352    for &p in points {
5353        let d = (p - center).length();
5354        if d > radius { let nr = (radius + d) * 0.5; center += (p - center).normalize() * (nr - radius); radius = nr; }
5355    }
5356    (center, radius)
5357}
5358
5359pub fn triangle_area(a: Vec3, b: Vec3, c: Vec3) -> f32 { (b - a).cross(c - a).length() * 0.5 }
5360
5361pub fn point_in_polygon_xz(point: Vec3, polygon: &[Vec3]) -> bool {
5362    let n = polygon.len(); if n < 3 { return false; }
5363    let mut inside = false; let mut j = n - 1;
5364    for i in 0..n {
5365        let (xi, zi, xj, zj) = (polygon[i].x, polygon[i].z, polygon[j].x, polygon[j].z);
5366        if ((zi > point.z) != (zj > point.z)) && (point.x < (xj - xi) * (point.z - zi) / (zj - zi) + xi) { inside = !inside; }
5367        j = i;
5368    }
5369    inside
5370}
5371
5372pub fn mesh_surface_area_2(mesh: &ParticleMesh, particles: &[ModelParticle]) -> f32 {
5373    mesh.faces.iter().map(|&[a, b, c]| triangle_area(particles[a].position, particles[b].position, particles[c].position)).sum()
5374}
5375
5376// ============================================================
5377// MODEL QUALITY METRICS
5378// ============================================================
5379
5380#[derive(Clone, Debug, Default)]
5381pub struct ModelQuality {
5382    pub particle_count:    usize,
5383    pub bounding_box_vol:  f32,
5384    pub density_variance:  f32,
5385    pub avg_neighbor_dist: f32,
5386    pub isolated_count:    usize,
5387    pub cluster_count:     usize,
5388    pub normal_consistency: f32,
5389}
5390
5391impl ModelQuality {
5392    pub fn analyze(model: &ParticleModel, radius: f32) -> Self {
5393        let mut q = Self::default();
5394        q.particle_count = model.particles.len();
5395        q.bounding_box_vol = model.bounds.volume();
5396        if model.particles.is_empty() { return q; }
5397        let r2 = radius * radius;
5398        let positions: Vec<Vec3> = model.particles.iter().map(|p| p.position).collect();
5399        let mut nc: Vec<usize> = vec![0; model.particles.len()];
5400        let (mut td, mut pc) = (0.0f32, 0usize);
5401        for i in 0..positions.len() {
5402            for j in (i+1)..positions.len() {
5403                let d2 = (positions[i] - positions[j]).length_squared();
5404                if d2 <= r2 { nc[i] += 1; nc[j] += 1; td += d2.sqrt(); pc += 1; }
5405            }
5406        }
5407        q.avg_neighbor_dist = if pc > 0 { td / pc as f32 } else { 0.0 };
5408        q.isolated_count = nc.iter().filter(|&&c| c == 0).count();
5409        let mean = nc.iter().sum::<usize>() as f32 / model.particles.len() as f32;
5410        q.density_variance = nc.iter().map(|&c| (c as f32 - mean).powi(2)).sum::<f32>() / model.particles.len() as f32;
5411        let mut nds = 0.0f32; let mut npc2 = 0usize;
5412        for i in 0..model.particles.len() {
5413            for j in (i+1)..model.particles.len() {
5414                if (positions[i] - positions[j]).length_squared() <= r2 {
5415                    nds += model.particles[i].normal.dot(model.particles[j].normal); npc2 += 1;
5416                }
5417            }
5418        }
5419        q.normal_consistency = if npc2 > 0 { nds / npc2 as f32 } else { 1.0 };
5420        let mut parent: Vec<usize> = (0..model.particles.len()).collect();
5421        fn find(p: &mut Vec<usize>, x: usize) -> usize { if p[x] != x { p[x] = find(p, p[x]); } p[x] }
5422        for i in 0..model.particles.len() {
5423            for j in (i+1)..model.particles.len() {
5424                if (positions[i] - positions[j]).length_squared() <= r2 {
5425                    let ri = find(&mut parent, i); let rj = find(&mut parent, j);
5426                    if ri != rj { parent[ri] = rj; }
5427                }
5428            }
5429        }
5430        q.cluster_count = (0..model.particles.len()).map(|i| find(&mut parent, i)).collect::<HashSet<_>>().len();
5431        q
5432    }
5433
5434    pub fn summary(&self) -> String {
5435        format!("Particles:{} Clusters:{} Isolated:{} AvgDist:{:.3} NormConsist:{:.3}",
5436            self.particle_count, self.cluster_count, self.isolated_count, self.avg_neighbor_dist, self.normal_consistency)
5437    }
5438}
5439
5440// ============================================================
5441// BATCH PROCESSOR
5442// ============================================================
5443
5444pub struct BatchProcessor;
5445
5446impl BatchProcessor {
5447    pub fn process_all<F>(model: &mut ParticleModel, mut f: F) where F: FnMut(usize, &mut ModelParticle) {
5448        for (i, p) in model.particles.iter_mut().enumerate() { f(i, p); }
5449    }
5450    pub fn process_selected<F>(model: &mut ParticleModel, sel: &HashSet<usize>, mut f: F) where F: FnMut(usize, &mut ModelParticle) {
5451        for &i in sel { if let Some(p) = model.particles.get_mut(i) { f(i, p); } }
5452    }
5453    pub fn remap_characters(model: &mut ParticleModel, map: &HashMap<char, char>) {
5454        for p in &mut model.particles { if let Some(&nc) = map.get(&p.character) { p.character = nc; } }
5455    }
5456    pub fn clamp_to_bounds(model: &mut ParticleModel, aabb: &Aabb3) {
5457        for p in &mut model.particles { p.position = clamp_vec3(p.position, aabb.min, aabb.max); }
5458        model.recompute_bounds();
5459    }
5460    pub fn normalize_colors(model: &mut ParticleModel) {
5461        for p in &mut model.particles {
5462            let m = p.color.x.max(p.color.y).max(p.color.z).max(EPSILON);
5463            p.color = Vec4::new(p.color.x / m, p.color.y / m, p.color.z / m, p.color.w);
5464        }
5465    }
5466    pub fn count_where<F>(model: &ParticleModel, mut f: F) -> usize where F: FnMut(&ModelParticle) -> bool {
5467        model.particles.iter().filter(|p| f(p)).count()
5468    }
5469    pub fn quantize_colors(model: &mut ParticleModel, palette: &[Vec4]) {
5470        if palette.is_empty() { return; }
5471        for p in &mut model.particles {
5472            let best = palette.iter()
5473                .min_by(|a, b| (**a - p.color).length_squared().partial_cmp(&(**b - p.color).length_squared())
5474                    .unwrap_or(std::cmp::Ordering::Equal))
5475                .copied().unwrap_or(p.color);
5476            p.color = best;
5477        }
5478    }
5479}
5480
5481// ============================================================
5482// LOD STREAMER
5483// ============================================================
5484
5485pub struct LodStreamer {
5486    pub models_by_distance: BTreeMap<u64, f32>,
5487    pub active_lods:         HashMap<u64, usize>,
5488    pub lod_bias:            f32,
5489}
5490
5491impl LodStreamer {
5492    pub fn new() -> Self { Self { models_by_distance: BTreeMap::new(), active_lods: HashMap::new(), lod_bias: 0.0 } }
5493
5494    pub fn register_model(&mut self, id: u64, dist: f32) {
5495        self.models_by_distance.insert(id, dist); self.active_lods.insert(id, 0);
5496    }
5497
5498    pub fn update_distances(&mut self, models: &HashMap<u64, ParticleModel>, camera: Vec3) {
5499        for (id, model) in models { self.models_by_distance.insert(*id, (model.bounds.center() - camera).length()); }
5500    }
5501
5502    pub fn select_lods(&mut self, models: &HashMap<u64, ParticleModel>) {
5503        for (id, &dist) in &self.models_by_distance {
5504            if let Some(m) = models.get(id) { self.active_lods.insert(*id, m.select_lod(dist, self.lod_bias)); }
5505        }
5506    }
5507
5508    pub fn get_lod(&self, id: u64) -> usize { self.active_lods.get(&id).copied().unwrap_or(0) }
5509
5510    pub fn models_in_range(&self, max_dist: f32) -> Vec<u64> {
5511        self.models_by_distance.iter().filter(|(_, &d)| d <= max_dist).map(|(&id, _)| id).collect()
5512    }
5513}
5514
5515impl Default for LodStreamer { fn default() -> Self { Self::new() } }
5516
5517// ============================================================
5518// UNIFIED TOOL CONTEXT 2
5519// ============================================================
5520
5521pub struct ModelingToolContext2 {
5522    pub editor:       ModelEditor,
5523    pub clipboard2:   Vec<ModelParticle>,
5524    pub clipboard_pivot: Vec3,
5525    pub presets2:     BTreeMap<String, BrushParams>,
5526    pub spline2:      Vec<Vec3>,
5527    pub material_lib: MaterialLibrary,
5528    pub group_mgr:    GroupManager,
5529    pub animations:   HashMap<String, ModelAnimation>,
5530}
5531
5532impl ModelingToolContext2 {
5533    pub fn new() -> Self {
5534        Self {
5535            editor: ModelEditor::new(),
5536            clipboard2: Vec::new(),
5537            clipboard_pivot: Vec3::ZERO,
5538            presets2: BTreeMap::new(),
5539            spline2: Vec::new(),
5540            material_lib: MaterialLibrary::new(),
5541            group_mgr: GroupManager::new(),
5542            animations: HashMap::new(),
5543        }
5544    }
5545
5546    pub fn copy_selection(&mut self) {
5547        let sel = self.editor.selection.clone();
5548        if let Some(m) = self.editor.active_model() {
5549            self.clipboard2 = sel.iter().filter_map(|&i| m.particles.get(i)).cloned().collect();
5550            self.clipboard_pivot = if self.clipboard2.is_empty() { Vec3::ZERO } else {
5551                self.clipboard2.iter().map(|p| p.position).fold(Vec3::ZERO, |a, b| a + b) / self.clipboard2.len() as f32
5552            };
5553        }
5554    }
5555
5556    pub fn paste_selection(&mut self, target: Vec3) {
5557        if self.clipboard2.is_empty() { return; }
5558        let offset = target - self.clipboard_pivot;
5559        let new_particles: Vec<ModelParticle> = self.clipboard2.iter().map(|p| {
5560            let mut np = p.clone(); np.position += offset; np
5561        }).collect();
5562        if let Some(m) = self.editor.active_model_mut() { m.add_particles_bulk(new_particles); }
5563    }
5564
5565    pub fn clipboard_count(&self) -> usize { self.clipboard2.len() }
5566
5567    pub fn add_animation(&mut self, name: impl Into<String>) -> String {
5568        let n = name.into();
5569        self.animations.insert(n.clone(), ModelAnimation::new(n.as_str()));
5570        n
5571    }
5572
5573    pub fn play_animation(&mut self, name: &str, time: f32) {
5574        if let Some(anim) = self.animations.get(name) {
5575            let anim = anim.clone();
5576            if let Some(m) = self.editor.active_model_mut() { anim.evaluate(time, m); }
5577        }
5578    }
5579
5580    pub fn apply_material(&mut self, material_name: &str, light: Vec3, view: Vec3) {
5581        if let Some(mat) = self.material_lib.get(material_name) {
5582            let mat = mat.clone();
5583            if let Some(m) = self.editor.active_model_mut() {
5584                for p in &mut m.particles { mat.apply_to_particle(p, light, view); }
5585            }
5586        }
5587    }
5588
5589    pub fn particle_count(&self) -> usize { self.editor.particle_count() }
5590
5591    pub fn add_spline_point(&mut self, p: Vec3) { self.spline2.push(p); }
5592    pub fn clear_spline(&mut self) { self.spline2.clear(); }
5593}
5594
5595impl Default for ModelingToolContext2 { fn default() -> Self { Self::new() } }
5596
5597// ============================================================
5598// INTEGRATION TESTS
5599// ============================================================
5600
5601#[cfg(test)]
5602mod integration_tests {
5603    use super::*;
5604
5605    #[test]
5606    fn test_kdtree_range() {
5607        let particles = PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 50, '.', Vec4::ONE);
5608        let tree = KdTree::build(&particles);
5609        let r = tree.range_search(Vec3::ZERO, 0.5);
5610        for &i in &r { assert!(particles[i].position.length() <= 1.0 + EPSILON); }
5611    }
5612
5613    #[test]
5614    fn test_knn_basic() {
5615        let particles = PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 50, '.', Vec4::ONE);
5616        let tree = KdTree::build(&particles);
5617        let knn = tree.k_nearest(Vec3::ZERO, 5);
5618        assert_eq!(knn.len(), 5);
5619    }
5620
5621    #[test]
5622    fn test_noise_range() {
5623        for i in 0..20 {
5624            let v = NoiseGenerator::value_3d(i as f32 * 0.3, i as f32 * 0.7, i as f32 * 0.5);
5625            assert!(v >= 0.0 && v <= 1.0, "v={}", v);
5626        }
5627    }
5628
5629    #[test]
5630    fn test_fbm_range() {
5631        for i in 0..10 {
5632            let v = NoiseGenerator::fbm(i as f32 * 0.5, i as f32 * 0.3, 0.7, 4, 2.0, 0.5);
5633            assert!(v >= 0.0 && v <= 1.0, "fbm={}", v);
5634        }
5635    }
5636
5637    #[test]
5638    fn test_stencil_circle() {
5639        let s = Stencil::circle("c", 32);
5640        assert!(s.sample(0.5, 0.5) > 0.8);
5641        assert!(s.sample(0.0, 0.0) < 0.2);
5642    }
5643
5644    #[test]
5645    fn test_easing_bounds() {
5646        for e in [EasingType::Linear, EasingType::EaseIn, EasingType::EaseOut, EasingType::EaseInOut] {
5647            assert!((e.apply(0.0) - 0.0).abs() < EPSILON, "{:?} at 0", e);
5648            assert!((e.apply(1.0) - 1.0).abs() < 0.01, "{:?} at 1={}", e, e.apply(1.0));
5649        }
5650    }
5651
5652    #[test]
5653    fn test_particle_delta_compute() {
5654        let before = vec![ModelParticle::new(Vec3::ZERO, '.', Vec4::ONE), ModelParticle::new(Vec3::X, '.', Vec4::ONE)];
5655        let mut after = before.clone(); after[0].position = Vec3::new(1.0, 0.0, 0.0);
5656        let d = ParticleDelta::compute(&before, &after);
5657        assert_eq!(d.modified.len(), 1);
5658    }
5659
5660    #[test]
5661    fn test_material_shade() {
5662        let mat = ParticleMaterial::new("t", MaterialType::Toon);
5663        let s = mat.shade(Vec3::Y, Vec3::Y, Vec3::Z);
5664        assert!(s.x > 0.5 || s.y > 0.5 || s.z > 0.5);
5665    }
5666
5667    #[test]
5668    fn test_voronoi_colors() {
5669        let mut model = ParticleModel::new(1, "v");
5670        model.add_particles_bulk(PrimitiveBuilder::plane(Vec3::ZERO, 4.0, 4.0, 10, 10, 0.0, 0.0, '.', Vec4::ONE));
5671        let seeds = vec![Vec3::new(-1.0, 0.0, -1.0), Vec3::new(1.0, 0.0, 1.0)];
5672        let colors = vec![Vec4::new(1.0, 0.0, 0.0, 1.0), Vec4::new(0.0, 0.0, 1.0, 1.0)];
5673        ProceduralPatterns::voronoi(&mut model, &seeds, &colors);
5674        for p in &model.particles {
5675            let r = (p.color - colors[0]).length() < 0.01;
5676            let b = (p.color - colors[1]).length() < 0.01;
5677            assert!(r || b, "unexpected color {:?}", p.color);
5678        }
5679    }
5680
5681    #[test]
5682    fn test_quality_analyze() {
5683        let mut model = ParticleModel::new(1, "q");
5684        model.add_particles_bulk(PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 30, '.', Vec4::ONE));
5685        model.recompute_bounds();
5686        let q = ModelQuality::analyze(&model, 0.5);
5687        assert_eq!(q.particle_count, 30);
5688        assert!(q.cluster_count >= 1);
5689    }
5690
5691    #[test]
5692    fn test_batch_remap() {
5693        let mut model = ParticleModel::new(1, "b");
5694        model.add_particle(ModelParticle::new(Vec3::ZERO, 'A', Vec4::ONE));
5695        let mut map = HashMap::new(); map.insert('A', 'X');
5696        BatchProcessor::remap_characters(&mut model, &map);
5697        assert_eq!(model.particles[0].character, 'X');
5698    }
5699
5700    #[test]
5701    fn test_group_manager() {
5702        let mut gm = GroupManager::new();
5703        let id = gm.create_group("fire", Vec4::new(1.0, 0.5, 0.0, 1.0));
5704        assert_eq!(gm.get_group(id).unwrap().name, "fire");
5705        gm.set_visibility(id, false);
5706        assert!(gm.visible_groups().is_empty());
5707    }
5708
5709    #[test]
5710    fn test_bounding_sphere_coverage() {
5711        let pts = vec![Vec3::new(1.0,0.0,0.0), Vec3::new(-1.0,0.0,0.0), Vec3::new(0.0,1.0,0.0), Vec3::new(0.0,-1.0,0.0)];
5712        let (c, r) = bounding_sphere(&pts);
5713        assert!(r >= 1.0 - EPSILON);
5714        for &p in &pts { assert!((p - c).length() <= r + 0.01); }
5715    }
5716
5717    #[test]
5718    fn test_sculpt_mask_operations() {
5719        let particles = PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 20, '.', Vec4::ONE);
5720        let mut mask = SculptMask::new(particles.len());
5721        mask.values[0] = 1.0;
5722        for i in 1..mask.count { mask.values[i] = 0.0; }
5723        mask.blur(&particles, 0.5, 3);
5724        let nonzero = mask.values.iter().filter(|&&v| v > 0.0).count();
5725        assert!(nonzero >= 1);
5726        let sel = mask.to_selection(0.01);
5727        assert!(!sel.is_empty());
5728    }
5729
5730    #[test]
5731    fn test_lod_streamer_basic() {
5732        let mut editor = ModelEditor::new();
5733        let id = editor.create_model("m");
5734        editor.insert_sphere(Vec3::ZERO, 1.0, 100);
5735        editor.generate_lods();
5736        let mut streamer = LodStreamer::new();
5737        streamer.register_model(id, 0.0);
5738        streamer.update_distances(&editor.models, Vec3::new(5.0, 0.0, 0.0));
5739        streamer.select_lods(&editor.models);
5740        assert!(streamer.get_lod(id) <= 3);
5741    }
5742
5743    #[test]
5744    fn test_modeling_context2_copy_paste() {
5745        let mut ctx = ModelingToolContext2::new();
5746        ctx.editor.create_model("ctx");
5747        ctx.editor.insert_sphere(Vec3::ZERO, 1.0, 40);
5748        assert_eq!(ctx.particle_count(), 40);
5749        ctx.editor.select_all();
5750        ctx.copy_selection();
5751        assert_eq!(ctx.clipboard_count(), 40);
5752        ctx.paste_selection(Vec3::new(3.0, 0.0, 0.0));
5753        assert_eq!(ctx.particle_count(), 80);
5754    }
5755
5756    #[test]
5757    fn test_particle_mesh_build() {
5758        let particles = PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 20, '.', Vec4::ONE);
5759        let mesh = ParticleMesh::build_from_particles(&particles, 0.8);
5760        assert!(!mesh.edges.is_empty());
5761        assert!(mesh.average_edge_length(&particles) > 0.0);
5762    }
5763
5764    #[test]
5765    fn test_remesher_decimate() {
5766        let mut model = ParticleModel::new(1, "r");
5767        model.add_particles_bulk(PrimitiveBuilder::sphere(Vec3::ZERO, 1.0, 100, '.', Vec4::ONE));
5768        let before = model.particles.len();
5769        Remesher::decimate(&mut model, 0.3);
5770        assert!(model.particles.len() < before);
5771    }
5772
5773    #[test]
5774    fn test_model_animation_evaluate() {
5775        let mut model = ParticleModel::new(1, "a");
5776        model.add_particle(ModelParticle::new(Vec3::ZERO, '.', Vec4::ONE));
5777        let mut anim = ModelAnimation::new("test");
5778        let snap0 = ModelSnapshot::capture(&model, "k0");
5779        model.particles[0].position = Vec3::new(1.0, 0.0, 0.0);
5780        let snap1 = ModelSnapshot::capture(&model, "k1");
5781        model.particles[0].position = Vec3::ZERO;
5782        anim.add_keyframe(0.0, snap0, EasingType::Linear);
5783        anim.add_keyframe(1.0, snap1, EasingType::Linear);
5784        anim.evaluate(0.5, &mut model);
5785        assert!((model.particles[0].position.x - 0.5).abs() < 0.01, "x={}", model.particles[0].position.x);
5786    }
5787
5788    #[test]
5789    fn test_reaction_diffusion_step() {
5790        let particles = PrimitiveBuilder::plane(Vec3::ZERO, 2.0, 2.0, 5, 5, 0.0, 0.0, '.', Vec4::ONE);
5791        let n = particles.len();
5792        let mut u = vec![1.0f32; n];
5793        let mut v = vec![0.0f32; n];
5794        v[0] = 0.5;
5795        ProceduralPatterns::reaction_diffusion_step(&mut u, &mut v, &particles, 1.0, 0.5, 0.055, 0.062, 0.1, 0.5);
5796        // u values should still be in [0,1]
5797        for &x in &u { assert!(x >= 0.0 && x <= 1.0); }
5798    }
5799
5800    #[test]
5801    fn test_euler_quat() {
5802        let q = euler_to_quat(0.0, 0.0, PI / 2.0);
5803        let (r, p, y) = quat_to_euler(q);
5804        assert!(y.abs() - PI / 2.0 < 0.01 || (r.abs() + p.abs()).abs() < 0.01);
5805    }
5806
5807    #[test]
5808    fn test_triangle_area() {
5809        let area = triangle_area(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
5810        assert!((area - 0.5).abs() < EPSILON);
5811    }
5812}
5813
5814// modeling_editor_ext3.rs — additional subsystems for the particle modeling editor
5815
5816// ──────────────────────────────────────────────────────────────────────────────
5817// SECTION 1: Constraint System
5818// ──────────────────────────────────────────────────────────────────────────────
5819
5820#[derive(Clone, Debug)]
5821pub enum ConstraintKind {
5822    FixedPosition,
5823    FixedNormal,
5824    OnSurface { surface_id: u64 },
5825    Distance { target_idx: usize, min_dist: f32, max_dist: f32 },
5826    Axis { axis: Vec3, origin: Vec3 },
5827    Plane { normal: Vec3, offset: f32 },
5828    Sphere { center: Vec3, radius: f32 },
5829    Cage { min: Vec3, max: Vec3 },
5830    Mirror { axis: u8 },  // 0=X,1=Y,2=Z
5831}
5832
5833#[derive(Clone, Debug)]
5834pub struct ParticleConstraint {
5835    pub particle_idx: usize,
5836    pub kind:         ConstraintKind,
5837    pub strength:     f32,
5838    pub enabled:      bool,
5839}
5840
5841impl ParticleConstraint {
5842    pub fn new(particle_idx: usize, kind: ConstraintKind) -> Self {
5843        Self { particle_idx, kind, strength: 1.0, enabled: true }
5844    }
5845
5846    pub fn apply(&self, pos: Vec3) -> Vec3 {
5847        if !self.enabled { return pos; }
5848        match &self.kind {
5849            ConstraintKind::FixedPosition => pos,
5850            ConstraintKind::FixedNormal   => pos,
5851            ConstraintKind::OnSurface { .. } => pos,
5852            ConstraintKind::Distance { target_idx: _, min_dist, max_dist } => {
5853                let len = pos.length();
5854                if len < *min_dist {
5855                    pos.normalize_or_zero() * *min_dist
5856                } else if len > *max_dist {
5857                    pos.normalize_or_zero() * *max_dist
5858                } else {
5859                    pos
5860                }
5861            }
5862            ConstraintKind::Axis { axis, origin } => {
5863                let d = pos - *origin;
5864                let proj = axis.dot(d);
5865                *origin + *axis * proj
5866            }
5867            ConstraintKind::Plane { normal, offset } => {
5868                let dist = normal.dot(pos) - offset;
5869                pos - *normal * dist * self.strength
5870            }
5871            ConstraintKind::Sphere { center, radius } => {
5872                let d = pos - *center;
5873                let len = d.length();
5874                if len > *radius {
5875                    *center + d.normalize_or_zero() * *radius
5876                } else {
5877                    pos
5878                }
5879            }
5880            ConstraintKind::Cage { min, max } => {
5881                Vec3::new(
5882                    pos.x.clamp(min.x, max.x),
5883                    pos.y.clamp(min.y, max.y),
5884                    pos.z.clamp(min.z, max.z),
5885                )
5886            }
5887            ConstraintKind::Mirror { axis } => {
5888                match axis {
5889                    0 => Vec3::new(pos.x.abs(), pos.y, pos.z),
5890                    1 => Vec3::new(pos.x, pos.y.abs(), pos.z),
5891                    2 => Vec3::new(pos.x, pos.y, pos.z.abs()),
5892                    _ => pos,
5893                }
5894            }
5895        }
5896    }
5897}
5898
5899pub struct ConstraintSolver {
5900    pub constraints: Vec<ParticleConstraint>,
5901    pub iterations:  u32,
5902}
5903
5904impl ConstraintSolver {
5905    pub fn new() -> Self {
5906        Self { constraints: Vec::new(), iterations: 4 }
5907    }
5908
5909    pub fn add_constraint(&mut self, c: ParticleConstraint) {
5910        self.constraints.push(c);
5911    }
5912
5913    pub fn remove_for_particle(&mut self, idx: usize) {
5914        self.constraints.retain(|c| c.particle_idx != idx);
5915    }
5916
5917    pub fn solve(&self, positions: &mut Vec<Vec3>) {
5918        for _ in 0..self.iterations {
5919            for c in &self.constraints {
5920                if c.particle_idx < positions.len() {
5921                    let old = positions[c.particle_idx];
5922                    positions[c.particle_idx] = c.apply(old);
5923                }
5924            }
5925        }
5926    }
5927
5928    pub fn solve_model(&self, model: &mut ParticleModel) {
5929        let mut positions: Vec<Vec3> = model.particles.iter().map(|p| p.position).collect();
5930        self.solve(&mut positions);
5931        for (i, p) in model.particles.iter_mut().enumerate() {
5932            p.position = positions[i];
5933        }
5934    }
5935}
5936
5937// ──────────────────────────────────────────────────────────────────────────────
5938// SECTION 2: Physics Simulation
5939// ──────────────────────────────────────────────────────────────────────────────
5940
5941#[derive(Clone, Debug)]
5942pub struct PhysicsParticle {
5943    pub position:     Vec3,
5944    pub velocity:     Vec3,
5945    pub acceleration: Vec3,
5946    pub mass:         f32,
5947    pub damping:      f32,
5948    pub fixed:        bool,
5949}
5950
5951impl PhysicsParticle {
5952    pub fn new(position: Vec3, mass: f32) -> Self {
5953        Self {
5954            position,
5955            velocity:     Vec3::ZERO,
5956            acceleration: Vec3::ZERO,
5957            mass,
5958            damping:      0.98,
5959            fixed:        false,
5960        }
5961    }
5962
5963    pub fn integrate(&mut self, dt: f32) {
5964        if self.fixed { return; }
5965        self.velocity = (self.velocity + self.acceleration * dt) * self.damping;
5966        self.position += self.velocity * dt;
5967        self.acceleration = Vec3::ZERO;
5968    }
5969
5970    pub fn apply_force(&mut self, force: Vec3) {
5971        if !self.fixed {
5972            self.acceleration += force / self.mass;
5973        }
5974    }
5975}
5976
5977#[derive(Clone, Debug)]
5978pub struct SpringConstraint {
5979    pub a:          usize,
5980    pub b:          usize,
5981    pub rest_len:   f32,
5982    pub stiffness:  f32,
5983    pub damping:    f32,
5984}
5985
5986impl SpringConstraint {
5987    pub fn new(a: usize, b: usize, rest_len: f32, stiffness: f32) -> Self {
5988        Self { a, b, rest_len, stiffness, damping: 0.01 }
5989    }
5990
5991    pub fn apply(&self, particles: &mut Vec<PhysicsParticle>) {
5992        if self.a >= particles.len() || self.b >= particles.len() { return; }
5993        let pa = particles[self.a].position;
5994        let pb = particles[self.b].position;
5995        let d  = pb - pa;
5996        let dist = d.length();
5997        if dist < 1e-6 { return; }
5998        let stretch = dist - self.rest_len;
5999        let dir     = d / dist;
6000        let force   = dir * stretch * self.stiffness;
6001        let va = particles[self.a].velocity;
6002        let vb = particles[self.b].velocity;
6003        let damp_force = (vb - va).dot(dir) * self.damping * dir;
6004        particles[self.a].apply_force( force + damp_force);
6005        particles[self.b].apply_force(-force - damp_force);
6006    }
6007}
6008
6009pub struct PhysicsSimulator {
6010    pub particles: Vec<PhysicsParticle>,
6011    pub springs:   Vec<SpringConstraint>,
6012    pub gravity:   Vec3,
6013    pub substeps:  u32,
6014    pub time:      f32,
6015}
6016
6017impl PhysicsSimulator {
6018    pub fn new() -> Self {
6019        Self {
6020            particles: Vec::new(),
6021            springs:   Vec::new(),
6022            gravity:   Vec3::new(0.0, -9.81, 0.0),
6023            substeps:  4,
6024            time:      0.0,
6025        }
6026    }
6027
6028    pub fn add_particle(&mut self, position: Vec3, mass: f32) -> usize {
6029        let idx = self.particles.len();
6030        self.particles.push(PhysicsParticle::new(position, mass));
6031        idx
6032    }
6033
6034    pub fn add_spring(&mut self, a: usize, b: usize, stiffness: f32) {
6035        let rest = if a < self.particles.len() && b < self.particles.len() {
6036            (self.particles[b].position - self.particles[a].position).length()
6037        } else {
6038            1.0
6039        };
6040        self.springs.push(SpringConstraint::new(a, b, rest, stiffness));
6041    }
6042
6043    pub fn step(&mut self, dt: f32) {
6044        let sub_dt = dt / self.substeps as f32;
6045        for _ in 0..self.substeps {
6046            // Apply gravity
6047            for p in &mut self.particles {
6048                p.apply_force(self.gravity * p.mass);
6049            }
6050            // Apply springs
6051            let springs = self.springs.clone();
6052            for s in &springs {
6053                s.apply(&mut self.particles);
6054            }
6055            // Integrate
6056            for p in &mut self.particles {
6057                p.integrate(sub_dt);
6058            }
6059        }
6060        self.time += dt;
6061    }
6062
6063    pub fn apply_to_model(&self, model: &mut ParticleModel) {
6064        for (i, p) in self.particles.iter().enumerate() {
6065            if i < model.particles.len() {
6066                model.particles[i].position = p.position;
6067            }
6068        }
6069    }
6070
6071    pub fn wind_force(&mut self, direction: Vec3, strength: f32, turbulence: f32) {
6072        for (i, p) in self.particles.iter_mut().enumerate() {
6073            let noise_val = ((i as f32 * 0.37 + self.time * 2.1).sin()
6074                + (i as f32 * 0.71 + self.time * 1.3).cos()) * 0.5;
6075            let t = Vec3::new(
6076                (i as f32 * 0.53 + self.time).sin(),
6077                (i as f32 * 0.29 + self.time * 1.7).cos(),
6078                (i as f32 * 0.61 + self.time * 0.9).sin(),
6079            ) * turbulence * noise_val;
6080            p.apply_force((direction + t) * strength * p.mass);
6081        }
6082    }
6083
6084    pub fn collision_floor(&mut self, y: f32, restitution: f32) {
6085        for p in &mut self.particles {
6086            if p.position.y < y {
6087                p.position.y = y;
6088                p.velocity.y = -p.velocity.y * restitution;
6089            }
6090        }
6091    }
6092
6093    pub fn collision_sphere(&mut self, center: Vec3, radius: f32, restitution: f32) {
6094        for p in &mut self.particles {
6095            let d = p.position - center;
6096            let dist = d.length();
6097            if dist < radius {
6098                let n = d.normalize_or_zero();
6099                p.position = center + n * radius;
6100                let vn = p.velocity.dot(n);
6101                if vn < 0.0 {
6102                    p.velocity -= n * vn * (1.0 + restitution);
6103                }
6104            }
6105        }
6106    }
6107}
6108
6109// ──────────────────────────────────────────────────────────────────────────────
6110// SECTION 3: Curve Tools
6111// ──────────────────────────────────────────────────────────────────────────────
6112
6113#[derive(Clone, Debug)]
6114pub enum CurveType {
6115    Polyline,
6116    CatmullRom { alpha: f32 },
6117    Bezier,
6118    BSpline { degree: usize },
6119    Nurbs { degree: usize, weights: Vec<f32> },
6120}
6121
6122#[derive(Clone, Debug)]
6123pub struct ModelCurve {
6124    pub control_points: Vec<Vec3>,
6125    pub curve_type:     CurveType,
6126    pub closed:         bool,
6127    pub resolution:     u32,
6128    pub name:           String,
6129}
6130
6131impl ModelCurve {
6132    pub fn new(name: &str, curve_type: CurveType) -> Self {
6133        Self {
6134            control_points: Vec::new(),
6135            curve_type,
6136            closed: false,
6137            resolution: 64,
6138            name: name.to_string(),
6139        }
6140    }
6141
6142    pub fn add_point(&mut self, p: Vec3) {
6143        self.control_points.push(p);
6144    }
6145
6146    pub fn evaluate(&self, t: f32) -> Vec3 {
6147        if self.control_points.is_empty() { return Vec3::ZERO; }
6148        if self.control_points.len() == 1 { return self.control_points[0]; }
6149        match &self.curve_type {
6150            CurveType::Polyline => self.eval_polyline(t),
6151            CurveType::CatmullRom { alpha } => self.eval_catmull_rom(t, *alpha),
6152            CurveType::Bezier => self.eval_bezier(t),
6153            CurveType::BSpline { degree } => self.eval_bspline(t, *degree),
6154            CurveType::Nurbs { degree, weights } => self.eval_nurbs(t, *degree, weights),
6155        }
6156    }
6157
6158    fn eval_polyline(&self, t: f32) -> Vec3 {
6159        let n = self.control_points.len() - 1;
6160        let scaled = t.clamp(0.0, 1.0) * n as f32;
6161        let i = (scaled as usize).min(n - 1);
6162        let f = scaled - i as f32;
6163        self.control_points[i].lerp(self.control_points[i + 1], f)
6164    }
6165
6166    fn eval_catmull_rom(&self, t: f32, alpha: f32) -> Vec3 {
6167        let pts = &self.control_points;
6168        let n = pts.len();
6169        if n < 2 { return pts[0]; }
6170        let scaled = t.clamp(0.0, 1.0) * (n - 1) as f32;
6171        let i1 = (scaled as usize).min(n - 2);
6172        let local_t = scaled - i1 as f32;
6173        let i0 = if i1 == 0 { 0 } else { i1 - 1 };
6174        let i2 = (i1 + 1).min(n - 1);
6175        let i3 = (i1 + 2).min(n - 1);
6176        let p0 = pts[i0]; let p1 = pts[i1]; let p2 = pts[i2]; let p3 = pts[i3];
6177        // Centripetal parameterization
6178        let t01 = (p1 - p0).length().powf(alpha);
6179        let t12 = (p2 - p1).length().powf(alpha);
6180        let t23 = (p3 - p2).length().powf(alpha);
6181        let m1 = if t01 + t12 > 1e-6 {
6182            (p2 - p1 + (p1 - p0) * (t12 / (t01 + 1e-6)) - (p2 - p0) * (t12 / (t01 + t12 + 1e-6))) * 0.5
6183        } else { p2 - p1 };
6184        let m2 = if t12 + t23 > 1e-6 {
6185            (p3 - p2 + (p2 - p1) * (t23 / (t12 + 1e-6)) - (p3 - p1) * (t23 / (t12 + t23 + 1e-6))) * 0.5
6186        } else { p2 - p1 };
6187        let u = local_t;
6188        let u2 = u * u; let u3 = u2 * u;
6189        p1 * (2.0*u3 - 3.0*u2 + 1.0)
6190            + m1 * (u3 - 2.0*u2 + u)
6191            + p2 * (-2.0*u3 + 3.0*u2)
6192            + m2 * (u3 - u2)
6193    }
6194
6195    fn eval_bezier(&self, t: f32) -> Vec3 {
6196        let pts = &self.control_points;
6197        let n = pts.len() - 1;
6198        let mut result = Vec3::ZERO;
6199        for (i, p) in pts.iter().enumerate() {
6200            let b = Self::bernstein(n, i, t);
6201            result += *p * b;
6202        }
6203        result
6204    }
6205
6206    fn bernstein(n: usize, i: usize, t: f32) -> f32 {
6207        Self::binomial(n, i) as f32 * t.powi(i as i32) * (1.0 - t).powi((n - i) as i32)
6208    }
6209
6210    fn binomial(n: usize, k: usize) -> u64 {
6211        if k > n { return 0; }
6212        let k = k.min(n - k);
6213        let mut result = 1u64;
6214        for i in 0..k {
6215            result = result * (n - i) as u64 / (i + 1) as u64;
6216        }
6217        result
6218    }
6219
6220    fn eval_bspline(&self, t: f32, degree: usize) -> Vec3 {
6221        let pts = &self.control_points;
6222        let n = pts.len();
6223        if n == 0 { return Vec3::ZERO; }
6224        let order = degree + 1;
6225        // Uniform knot vector
6226        let num_knots = n + order;
6227        let knots: Vec<f32> = (0..num_knots).map(|i| i as f32 / (num_knots - 1) as f32).collect();
6228        let t_clamped = t.clamp(knots[degree], knots[n]);
6229        // De Boor's algorithm
6230        let mut k = degree;
6231        for i in degree..(n + degree) {
6232            if t_clamped >= knots[i] && t_clamped < knots[i + 1] {
6233                k = i;
6234                break;
6235            }
6236        }
6237        let mut d: Vec<Vec3> = (0..=degree).map(|j| {
6238            let idx = j + k - degree;
6239            if idx < n { pts[idx] } else { Vec3::ZERO }
6240        }).collect();
6241        for r in 1..=degree {
6242            for j in (r..=degree).rev() {
6243                let kj = j + k - degree;
6244                let denom = knots[kj + degree - r + 1] - knots[kj];
6245                let alpha = if denom.abs() > 1e-9 {
6246                    (t_clamped - knots[kj]) / denom
6247                } else { 0.0 };
6248                d[j] = d[j - 1].lerp(d[j], alpha);
6249            }
6250        }
6251        d[degree]
6252    }
6253
6254    fn eval_nurbs(&self, t: f32, degree: usize, weights: &[f32]) -> Vec3 {
6255        let pts = &self.control_points;
6256        let n = pts.len().min(weights.len());
6257        if n == 0 { return Vec3::ZERO; }
6258        // Homogeneous coordinates
6259        let order = degree + 1;
6260        let num_knots = n + order;
6261        let knots: Vec<f32> = (0..num_knots).map(|i| i as f32 / (num_knots - 1) as f32).collect();
6262        let t_c = t.clamp(knots[degree], knots[n]);
6263        let mut k = degree;
6264        for i in degree..(n + degree) {
6265            if t_c >= knots[i] && t_c < knots[i + 1] { k = i; break; }
6266        }
6267        // Weighted homogeneous de Boor
6268        let mut hw: Vec<Vec4> = (0..=degree).map(|j| {
6269            let idx = (j + k - degree).min(n - 1);
6270            let w = weights[idx];
6271            Vec4::new(pts[idx].x * w, pts[idx].y * w, pts[idx].z * w, w)
6272        }).collect();
6273        for r in 1..=degree {
6274            for j in (r..=degree).rev() {
6275                let kj = j + k - degree;
6276                let denom = knots[kj + degree - r + 1] - knots[kj];
6277                let alpha = if denom.abs() > 1e-9 { (t_c - knots[kj]) / denom } else { 0.0 };
6278                hw[j] = hw[j - 1] + (hw[j] - hw[j - 1]) * alpha;
6279            }
6280        }
6281        let w = hw[degree].w;
6282        if w.abs() < 1e-9 { return Vec3::ZERO; }
6283        Vec3::new(hw[degree].x / w, hw[degree].y / w, hw[degree].z / w)
6284    }
6285
6286    /// Sample the curve into N points
6287    pub fn sample(&self, n: u32) -> Vec<Vec3> {
6288        (0..n).map(|i| {
6289            let t = i as f32 / (n - 1).max(1) as f32;
6290            self.evaluate(t)
6291        }).collect()
6292    }
6293
6294    /// Extrude curve into particles along the path
6295    pub fn extrude_to_particles(&self, char_: char, color: Vec4, spacing: f32) -> Vec<ModelParticle> {
6296        let pts = self.sample(self.resolution);
6297        let mut particles = Vec::new();
6298        let mut dist = 0.0_f32;
6299        for i in 1..pts.len() {
6300            let seg_len = (pts[i] - pts[i - 1]).length();
6301            while dist <= seg_len {
6302                let t = dist / seg_len.max(1e-6);
6303                let pos = pts[i - 1].lerp(pts[i], t);
6304                let tangent = (pts[i] - pts[i - 1]).normalize_or_zero();
6305                particles.push(ModelParticle {
6306                    position:     pos,
6307                    character:    char_,
6308                    color,
6309                    emission:     0.0,
6310                    normal:       tangent,
6311                    bone_weights: [1.0, 0.0, 0.0, 0.0],
6312                    bone_indices: [0, 0, 0, 0],
6313                    group_id:     0,
6314                    layer_id:     0,
6315                    selected:     false,
6316                    locked:       false,
6317                });
6318                dist += spacing;
6319            }
6320            dist -= seg_len;
6321        }
6322        particles
6323    }
6324
6325    /// Compute arc length
6326    pub fn arc_length(&self, samples: u32) -> f32 {
6327        let pts = self.sample(samples);
6328        let mut len = 0.0_f32;
6329        for i in 1..pts.len() {
6330            len += (pts[i] - pts[i - 1]).length();
6331        }
6332        len
6333    }
6334
6335    /// Closest point on curve to a query point
6336    pub fn closest_point(&self, query: Vec3, samples: u32) -> (Vec3, f32) {
6337        let pts = self.sample(samples);
6338        let mut best = pts[0];
6339        let mut best_t = 0.0_f32;
6340        let mut best_d2 = f32::MAX;
6341        for (i, p) in pts.iter().enumerate() {
6342            let d2 = (*p - query).length_squared();
6343            if d2 < best_d2 {
6344                best_d2 = d2;
6345                best = *p;
6346                best_t = i as f32 / (samples - 1).max(1) as f32;
6347            }
6348        }
6349        (best, best_t)
6350    }
6351
6352    /// Frenet-Serret frame at parameter t
6353    pub fn frenet_frame(&self, t: f32) -> (Vec3, Vec3, Vec3) {
6354        let eps = 0.001_f32;
6355        let p0 = self.evaluate((t - eps).max(0.0));
6356        let p1 = self.evaluate((t + eps).min(1.0));
6357        let tangent = (p1 - p0).normalize_or_zero();
6358        let p2 = self.evaluate((t - 2.0 * eps).max(0.0));
6359        let p3 = self.evaluate((t + 2.0 * eps).min(1.0));
6360        let accel = p3 - 2.0 * self.evaluate(t) + p2;
6361        let normal = if accel.length_squared() > 1e-9 {
6362            (accel - tangent * tangent.dot(accel)).normalize_or_zero()
6363        } else {
6364            let up = if tangent.y.abs() < 0.9 { Vec3::Y } else { Vec3::X };
6365            tangent.cross(up).normalize_or_zero()
6366        };
6367        let binormal = tangent.cross(normal).normalize_or_zero();
6368        (tangent, normal, binormal)
6369    }
6370}
6371
6372pub struct CurveLibrary {
6373    pub curves: HashMap<String, ModelCurve>,
6374}
6375
6376impl CurveLibrary {
6377    pub fn new() -> Self { Self { curves: HashMap::new() } }
6378    pub fn add(&mut self, curve: ModelCurve) { self.curves.insert(curve.name.clone(), curve); }
6379    pub fn get(&self, name: &str) -> Option<&ModelCurve> { self.curves.get(name) }
6380    pub fn remove(&mut self, name: &str) -> Option<ModelCurve> { self.curves.remove(name) }
6381    pub fn names(&self) -> Vec<&String> { self.curves.keys().collect() }
6382}
6383
6384// ──────────────────────────────────────────────────────────────────────────────
6385// SECTION 4: Texture Projection
6386// ──────────────────────────────────────────────────────────────────────────────
6387
6388#[derive(Clone, Debug)]
6389pub enum ProjectionMode {
6390    Planar  { normal: Vec3, up: Vec3, origin: Vec3 },
6391    Spherical { center: Vec3 },
6392    Cylindrical { axis: Vec3, origin: Vec3 },
6393    Cubic   { scale: f32 },
6394    Camera  { view_proj: Mat4 },
6395}
6396
6397pub struct TextureProjector {
6398    pub mode:    ProjectionMode,
6399    pub scale:   Vec2,
6400    pub offset:  Vec2,
6401    pub rotation: f32,
6402    pub flip_u:  bool,
6403    pub flip_v:  bool,
6404}
6405
6406impl TextureProjector {
6407    pub fn new(mode: ProjectionMode) -> Self {
6408        Self { mode, scale: Vec2::ONE, offset: Vec2::ZERO, rotation: 0.0, flip_u: false, flip_v: false }
6409    }
6410
6411    pub fn project(&self, pos: Vec3) -> Vec2 {
6412        let uv = match &self.mode {
6413            ProjectionMode::Planar { normal, up, origin } => {
6414                let right = up.cross(*normal).normalize_or_zero();
6415                let local = pos - *origin;
6416                Vec2::new(local.dot(right), local.dot(*up))
6417            }
6418            ProjectionMode::Spherical { center } => {
6419                let d = (pos - *center).normalize_or_zero();
6420                let u = 0.5 + d.z.atan2(d.x) / (2.0 * std::f32::consts::PI);
6421                let v = 0.5 - d.y.asin() / std::f32::consts::PI;
6422                Vec2::new(u, v)
6423            }
6424            ProjectionMode::Cylindrical { axis, origin } => {
6425                let d = pos - *origin;
6426                let height = d.dot(*axis);
6427                let radial = d - *axis * height;
6428                let angle = radial.z.atan2(radial.x);
6429                Vec2::new(angle / (2.0 * std::f32::consts::PI) + 0.5, height)
6430            }
6431            ProjectionMode::Cubic { scale } => {
6432                let p = pos * *scale;
6433                Vec2::new(p.x.fract(), p.y.fract())
6434            }
6435            ProjectionMode::Camera { view_proj } => {
6436                let clip = *view_proj * Vec4::new(pos.x, pos.y, pos.z, 1.0);
6437                let ndc = if clip.w.abs() > 1e-6 {
6438                    Vec2::new(clip.x / clip.w, clip.y / clip.w)
6439                } else { Vec2::ZERO };
6440                (ndc + Vec2::ONE) * 0.5
6441            }
6442        };
6443        // Apply rotation
6444        let cos_r = self.rotation.cos();
6445        let sin_r = self.rotation.sin();
6446        let centered = uv - Vec2::splat(0.5);
6447        let rotated  = Vec2::new(
6448            centered.x * cos_r - centered.y * sin_r,
6449            centered.x * sin_r + centered.y * cos_r,
6450        );
6451        let uv2 = (rotated + Vec2::splat(0.5)) * self.scale + self.offset;
6452        Vec2::new(
6453            if self.flip_u { 1.0 - uv2.x } else { uv2.x },
6454            if self.flip_v { 1.0 - uv2.y } else { uv2.y },
6455        )
6456    }
6457
6458    pub fn apply_to_model_colors(&self, model: &mut ParticleModel, palette: &[Vec4]) {
6459        if palette.is_empty() { return; }
6460        for p in &mut model.particles {
6461            let uv = self.project(p.position);
6462            let ux = uv.x.fract().abs();
6463            let uy = uv.y.fract().abs();
6464            // Map UV to palette using Halton-like 2D index
6465            let px = ((ux * palette.len() as f32) as usize).min(palette.len() - 1);
6466            let py = ((uy * palette.len() as f32) as usize).min(palette.len() - 1);
6467            let idx = (px + py) % palette.len();
6468            p.color = palette[idx];
6469        }
6470    }
6471
6472    pub fn apply_to_model_chars(&self, model: &mut ParticleModel, char_set: &[char]) {
6473        if char_set.is_empty() { return; }
6474        for p in &mut model.particles {
6475            let uv = self.project(p.position);
6476            let ux = uv.x.fract().abs();
6477            let idx = ((ux * char_set.len() as f32) as usize).min(char_set.len() - 1);
6478            p.character = char_set[idx];
6479        }
6480    }
6481}
6482
6483// ──────────────────────────────────────────────────────────────────────────────
6484// SECTION 5: Particle Field Effects
6485// ──────────────────────────────────────────────────────────────────────────────
6486
6487#[derive(Clone, Debug)]
6488pub enum FieldType {
6489    Gravitational { center: Vec3, strength: f32 },
6490    Magnetic      { axis: Vec3, origin: Vec3, strength: f32 },
6491    Wind          { direction: Vec3, strength: f32, turbulence: f32 },
6492    Vortex        { axis: Vec3, origin: Vec3, angular_vel: f32, decay: f32 },
6493    Repulsion     { center: Vec3, radius: f32, strength: f32 },
6494    Attraction    { center: Vec3, radius: f32, strength: f32 },
6495    Turbulent     { scale: f32, strength: f32, time_offset: f32 },
6496    Shockwave     { origin: Vec3, speed: f32, strength: f32, time: f32 },
6497}
6498
6499impl FieldType {
6500    pub fn evaluate(&self, pos: Vec3, time: f32) -> Vec3 {
6501        match self {
6502            FieldType::Gravitational { center, strength } => {
6503                let d = *center - pos;
6504                let d2 = d.length_squared();
6505                if d2 < 1e-6 { return Vec3::ZERO; }
6506                d.normalize_or_zero() * *strength / d2
6507            }
6508            FieldType::Magnetic { axis, origin, strength } => {
6509                let d = pos - *origin;
6510                let along = axis.dot(d);
6511                let perp = d - *axis * along;
6512                axis.cross(perp) * *strength
6513            }
6514            FieldType::Wind { direction, strength, turbulence } => {
6515                let t = time;
6516                let noise = Vec3::new(
6517                    (pos.x * 0.5 + t).sin() * (pos.z * 0.3).cos(),
6518                    (pos.y * 0.4 + t * 1.3).sin(),
6519                    (pos.z * 0.6 + t * 0.7).cos(),
6520                ) * *turbulence;
6521                *direction * *strength + noise
6522            }
6523            FieldType::Vortex { axis, origin, angular_vel, decay } => {
6524                let d = pos - *origin;
6525                let along = axis.dot(d);
6526                let perp = d - *axis * along;
6527                let r = perp.length();
6528                if r < 1e-6 { return Vec3::ZERO; }
6529                let tangent = axis.cross(perp).normalize_or_zero();
6530                let speed = *angular_vel * (-r * *decay).exp();
6531                tangent * speed
6532            }
6533            FieldType::Repulsion { center, radius, strength } => {
6534                let d = pos - *center;
6535                let dist = d.length();
6536                if dist > *radius || dist < 1e-6 { return Vec3::ZERO; }
6537                let falloff = 1.0 - dist / *radius;
6538                d.normalize_or_zero() * *strength * falloff * falloff
6539            }
6540            FieldType::Attraction { center, radius, strength } => {
6541                let d = *center - pos;
6542                let dist = d.length();
6543                if dist > *radius || dist < 1e-6 { return Vec3::ZERO; }
6544                let falloff = 1.0 - dist / *radius;
6545                d.normalize_or_zero() * *strength * falloff
6546            }
6547            FieldType::Turbulent { scale, strength, time_offset } => {
6548                let s = *scale;
6549                let t = time + *time_offset;
6550                Vec3::new(
6551                    (pos.x * s + t * 1.1).sin() * (pos.y * s * 0.7).cos(),
6552                    (pos.y * s + t * 0.8).sin() * (pos.z * s * 1.3).cos(),
6553                    (pos.z * s + t * 1.5).sin() * (pos.x * s * 0.9).cos(),
6554                ) * *strength
6555            }
6556            FieldType::Shockwave { origin, speed, strength, time } => {
6557                let elapsed = time;
6558                let radius = speed * elapsed;
6559                let d = pos - *origin;
6560                let dist = d.length();
6561                let wave_width = 0.5_f32;
6562                let diff = (dist - radius).abs();
6563                if diff > wave_width { return Vec3::ZERO; }
6564                let falloff = 1.0 - diff / wave_width;
6565                d.normalize_or_zero() * *strength * falloff
6566            }
6567        }
6568    }
6569}
6570
6571pub struct ParticleField {
6572    pub fields:    Vec<FieldType>,
6573    pub time:      f32,
6574    pub enabled:   bool,
6575}
6576
6577impl ParticleField {
6578    pub fn new() -> Self { Self { fields: Vec::new(), time: 0.0, enabled: true } }
6579    pub fn add(&mut self, f: FieldType) { self.fields.push(f); }
6580    pub fn clear(&mut self) { self.fields.clear(); }
6581
6582    pub fn evaluate(&self, pos: Vec3) -> Vec3 {
6583        if !self.enabled { return Vec3::ZERO; }
6584        let mut total = Vec3::ZERO;
6585        for f in &self.fields {
6586            total += f.evaluate(pos, self.time);
6587        }
6588        total
6589    }
6590
6591    pub fn apply_displacement(&self, model: &mut ParticleModel, dt: f32, max_disp: f32) {
6592        if !self.enabled { return; }
6593        for p in &mut model.particles {
6594            if p.locked { continue; }
6595            let force = self.evaluate(p.position);
6596            let disp = force * dt;
6597            let disp_len = disp.length();
6598            if disp_len > max_disp {
6599                p.position += disp / disp_len * max_disp;
6600            } else {
6601                p.position += disp;
6602            }
6603        }
6604    }
6605
6606    pub fn apply_color_modulation(&self, model: &mut ParticleModel) {
6607        for p in &mut model.particles {
6608            let force = self.evaluate(p.position);
6609            let intensity = (force.length() * 0.1).min(1.0);
6610            p.color = Vec4::new(
6611                (p.color.x + intensity * 0.1).min(1.0),
6612                (p.color.y - intensity * 0.05).max(0.0),
6613                (p.color.z + intensity * 0.2).min(1.0),
6614                p.color.w,
6615            );
6616        }
6617    }
6618}
6619
6620// ──────────────────────────────────────────────────────────────────────────────
6621// SECTION 6: Render Pipeline
6622// ──────────────────────────────────────────────────────────────────────────────
6623
6624#[derive(Clone, Debug)]
6625pub struct RenderCell {
6626    pub character: char,
6627    pub fg_color:  Vec4,
6628    pub bg_color:  Vec4,
6629    pub bold:      bool,
6630    pub italic:    bool,
6631    pub depth:     f32,
6632}
6633
6634impl Default for RenderCell {
6635    fn default() -> Self {
6636        Self {
6637            character: ' ',
6638            fg_color:  Vec4::ONE,
6639            bg_color:  Vec4::ZERO,
6640            bold:      false,
6641            italic:    false,
6642            depth:     f32::MAX,
6643        }
6644    }
6645}
6646
6647#[derive(Clone, Debug)]
6648pub struct RenderBuffer {
6649    pub width:  usize,
6650    pub height: usize,
6651    pub cells:  Vec<RenderCell>,
6652    pub depth:  Vec<f32>,
6653}
6654
6655impl RenderBuffer {
6656    pub fn new(width: usize, height: usize) -> Self {
6657        let n = width * height;
6658        Self {
6659            width,
6660            height,
6661            cells: vec![RenderCell::default(); n],
6662            depth: vec![f32::MAX; n],
6663        }
6664    }
6665
6666    pub fn clear(&mut self) {
6667        for c in &mut self.cells { *c = RenderCell::default(); }
6668        for d in &mut self.depth { *d = f32::MAX; }
6669    }
6670
6671    pub fn set(&mut self, x: usize, y: usize, cell: RenderCell) {
6672        if x < self.width && y < self.height {
6673            let idx = y * self.width + x;
6674            if cell.depth < self.depth[idx] {
6675                self.depth[idx] = cell.depth;
6676                self.cells[idx] = cell;
6677            }
6678        }
6679    }
6680
6681    pub fn get(&self, x: usize, y: usize) -> Option<&RenderCell> {
6682        if x < self.width && y < self.height {
6683            Some(&self.cells[y * self.width + x])
6684        } else { None }
6685    }
6686
6687    pub fn composite(&mut self, other: &RenderBuffer) {
6688        for y in 0..self.height.min(other.height) {
6689            for x in 0..self.width.min(other.width) {
6690                if let Some(c) = other.get(x, y) {
6691                    if c.character != ' ' {
6692                        self.set(x, y, c.clone());
6693                    }
6694                }
6695            }
6696        }
6697    }
6698}
6699
6700#[derive(Clone, Debug)]
6701pub struct RenderCamera {
6702    pub position:   Vec3,
6703    pub target:     Vec3,
6704    pub up:         Vec3,
6705    pub fov:        f32,
6706    pub near:       f32,
6707    pub far:        f32,
6708    pub ortho:      bool,
6709    pub ortho_size: f32,
6710}
6711
6712impl RenderCamera {
6713    pub fn new() -> Self {
6714        Self {
6715            position:   Vec3::new(0.0, 5.0, 10.0),
6716            target:     Vec3::ZERO,
6717            up:         Vec3::Y,
6718            fov:        60.0_f32.to_radians(),
6719            near:       0.1,
6720            far:        1000.0,
6721            ortho:      false,
6722            ortho_size: 10.0,
6723        }
6724    }
6725
6726    pub fn view_matrix(&self) -> Mat4 {
6727        Mat4::look_at_rh(self.position, self.target, self.up)
6728    }
6729
6730    pub fn proj_matrix(&self, aspect: f32) -> Mat4 {
6731        if self.ortho {
6732            let h = self.ortho_size * 0.5;
6733            let w = h * aspect;
6734            Mat4::orthographic_rh(-w, w, -h, h, self.near, self.far)
6735        } else {
6736            Mat4::perspective_rh(self.fov, aspect, self.near, self.far)
6737        }
6738    }
6739
6740    pub fn world_to_screen(&self, pos: Vec3, width: u32, height: u32) -> Option<(i32, i32, f32)> {
6741        let aspect = width as f32 / height as f32;
6742        let vp = self.proj_matrix(aspect) * self.view_matrix();
6743        let clip = vp * Vec4::new(pos.x, pos.y, pos.z, 1.0);
6744        if clip.w.abs() < 1e-6 { return None; }
6745        let ndc = Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
6746        if ndc.z < -1.0 || ndc.z > 1.0 { return None; }
6747        let sx = ((ndc.x + 1.0) * 0.5 * width  as f32) as i32;
6748        let sy = ((1.0 - ndc.y) * 0.5 * height as f32) as i32;
6749        Some((sx, sy, ndc.z))
6750    }
6751
6752    pub fn orbit(&mut self, delta_yaw: f32, delta_pitch: f32) {
6753        let arm = self.position - self.target;
6754        let radius = arm.length();
6755        let yaw   = arm.z.atan2(arm.x) + delta_yaw;
6756        let pitch = (arm.y / radius.max(1e-6)).asin() + delta_pitch;
6757        let pitch = pitch.clamp(-1.5, 1.5);
6758        self.position = self.target + Vec3::new(
6759            radius * pitch.cos() * yaw.cos(),
6760            radius * pitch.sin(),
6761            radius * pitch.cos() * yaw.sin(),
6762        );
6763    }
6764
6765    pub fn dolly(&mut self, delta: f32) {
6766        let dir = (self.target - self.position).normalize_or_zero();
6767        self.position += dir * delta;
6768    }
6769
6770    pub fn pan(&mut self, dx: f32, dy: f32) {
6771        let fwd   = (self.target - self.position).normalize_or_zero();
6772        let right = fwd.cross(self.up).normalize_or_zero();
6773        let up    = right.cross(fwd).normalize_or_zero();
6774        let delta = right * dx + up * dy;
6775        self.position += delta;
6776        self.target   += delta;
6777    }
6778}
6779
6780pub struct ParticleRenderer {
6781    pub camera:       RenderCamera,
6782    pub buffer:       RenderBuffer,
6783    pub show_normals: bool,
6784    pub show_bones:   bool,
6785    pub show_grid:    bool,
6786    pub grid_size:    f32,
6787    pub ambient:      f32,
6788    pub light_dir:    Vec3,
6789}
6790
6791impl ParticleRenderer {
6792    pub fn new(width: usize, height: usize) -> Self {
6793        Self {
6794            camera:       RenderCamera::new(),
6795            buffer:       RenderBuffer::new(width, height),
6796            show_normals: false,
6797            show_bones:   false,
6798            show_grid:    true,
6799            grid_size:    1.0,
6800            ambient:      0.2,
6801            light_dir:    Vec3::new(0.5, 1.0, 0.3).normalize_or_zero(),
6802        }
6803    }
6804
6805    pub fn render_model(&mut self, model: &ParticleModel) {
6806        let w = self.buffer.width as u32;
6807        let h = self.buffer.height as u32;
6808        for p in &model.particles {
6809            if let Some((sx, sy, depth)) = self.camera.world_to_screen(p.position, w, h) {
6810                if sx < 0 || sy < 0 || sx >= w as i32 || sy >= h as i32 { continue; }
6811                let diffuse = self.light_dir.dot(p.normal).max(0.0);
6812                let light   = self.ambient + diffuse * (1.0 - self.ambient);
6813                let lit_color = Vec4::new(
6814                    (p.color.x * light).min(1.0),
6815                    (p.color.y * light).min(1.0),
6816                    (p.color.z * light).min(1.0),
6817                    p.color.w,
6818                );
6819                let cell = RenderCell {
6820                    character: if p.selected { '*' } else { p.character },
6821                    fg_color:  lit_color,
6822                    bg_color:  Vec4::ZERO,
6823                    bold:      p.selected,
6824                    italic:    false,
6825                    depth,
6826                };
6827                self.buffer.set(sx as usize, sy as usize, cell);
6828            }
6829        }
6830    }
6831
6832    pub fn render_grid(&mut self) {
6833        if !self.show_grid { return; }
6834        let w = self.buffer.width as u32;
6835        let h = self.buffer.height as u32;
6836        let half = 10.0_f32;
6837        let step = self.grid_size;
6838        let mut x = -half;
6839        while x <= half {
6840            let mut z = -half;
6841            while z <= half {
6842                let pos = Vec3::new(x, 0.0, z);
6843                if let Some((sx, sy, d)) = self.camera.world_to_screen(pos, w, h) {
6844                    if sx >= 0 && sy >= 0 && sx < w as i32 && sy < h as i32 {
6845                        self.buffer.set(sx as usize, sy as usize, RenderCell {
6846                            character: '.',
6847                            fg_color:  Vec4::new(0.3, 0.3, 0.3, 1.0),
6848                            bg_color:  Vec4::ZERO,
6849                            bold:      false,
6850                            italic:    false,
6851                            depth:     d,
6852                        });
6853                    }
6854                }
6855                z += step;
6856            }
6857            x += step;
6858        }
6859    }
6860
6861    pub fn resize(&mut self, width: usize, height: usize) {
6862        self.buffer = RenderBuffer::new(width, height);
6863    }
6864}
6865
6866// ──────────────────────────────────────────────────────────────────────────────
6867// SECTION 7: Model Comparison and Diffing
6868// ──────────────────────────────────────────────────────────────────────────────
6869
6870#[derive(Clone, Debug)]
6871pub struct ParticleDiff {
6872    pub added:   Vec<ModelParticle>,
6873    pub removed: Vec<usize>,
6874    pub moved:   Vec<(usize, Vec3, Vec3)>,
6875    pub recolored: Vec<(usize, Vec4, Vec4)>,
6876}
6877
6878impl ParticleDiff {
6879    pub fn compute(before: &ParticleModel, after: &ParticleModel) -> Self {
6880        let before_n = before.particles.len();
6881        let after_n  = after.particles.len();
6882        let mut moved: Vec<(usize, Vec3, Vec3)> = Vec::new();
6883        let mut recolored: Vec<(usize, Vec4, Vec4)> = Vec::new();
6884        let common = before_n.min(after_n);
6885        for i in 0..common {
6886            let bp = &before.particles[i];
6887            let ap = &after.particles[i];
6888            if (bp.position - ap.position).length_squared() > 1e-8 {
6889                moved.push((i, bp.position, ap.position));
6890            }
6891            if (bp.color - ap.color).length_squared() > 1e-8 {
6892                recolored.push((i, bp.color, ap.color));
6893            }
6894        }
6895        let added: Vec<ModelParticle> = if after_n > before_n {
6896            after.particles[before_n..].to_vec()
6897        } else { Vec::new() };
6898        let removed: Vec<usize> = if before_n > after_n {
6899            (after_n..before_n).collect()
6900        } else { Vec::new() };
6901        Self { added, removed, moved, recolored }
6902    }
6903
6904    pub fn apply(&self, model: &mut ParticleModel) {
6905        // Remove in reverse order
6906        let mut to_remove = self.removed.clone();
6907        to_remove.sort_unstable_by(|a, b| b.cmp(a));
6908        for idx in &to_remove {
6909            if *idx < model.particles.len() {
6910                model.particles.remove(*idx);
6911            }
6912        }
6913        // Apply moves
6914        for (i, _from, to) in &self.moved {
6915            if *i < model.particles.len() {
6916                model.particles[*i].position = *to;
6917            }
6918        }
6919        // Apply recolors
6920        for (i, _from, to) in &self.recolored {
6921            if *i < model.particles.len() {
6922                model.particles[*i].color = *to;
6923            }
6924        }
6925        // Add new
6926        for p in &self.added {
6927            model.particles.push(p.clone());
6928        }
6929    }
6930
6931    pub fn invert(&self) -> Self {
6932        Self {
6933            added:     Vec::new(),
6934            removed:   (0..self.added.len()).collect(),
6935            moved:     self.moved.iter().map(|(i, f, t)| (*i, *t, *f)).collect(),
6936            recolored: self.recolored.iter().map(|(i, f, t)| (*i, *t, *f)).collect(),
6937        }
6938    }
6939
6940    pub fn summary(&self) -> String {
6941        format!(
6942            "added={}, removed={}, moved={}, recolored={}",
6943            self.added.len(), self.removed.len(), self.moved.len(), self.recolored.len()
6944        )
6945    }
6946}
6947
6948// ──────────────────────────────────────────────────────────────────────────────
6949// SECTION 8: Spatial Partitioning — Octree
6950// ──────────────────────────────────────────────────────────────────────────────
6951
6952#[derive(Clone, Debug)]
6953pub struct OctreeNode {
6954    pub center:   Vec3,
6955    pub half:     f32,
6956    pub indices:  Vec<usize>,
6957    pub children: Option<Box<[OctreeNode; 8]>>,
6958}
6959
6960impl OctreeNode {
6961    const MAX_CAPACITY: usize = 16;
6962    const MAX_DEPTH:    u32   = 8;
6963
6964    pub fn new(center: Vec3, half: f32) -> Self {
6965        Self { center, half, indices: Vec::new(), children: None }
6966    }
6967
6968    pub fn contains(&self, p: Vec3) -> bool {
6969        let d = p - self.center;
6970        d.x.abs() <= self.half && d.y.abs() <= self.half && d.z.abs() <= self.half
6971    }
6972
6973    pub fn insert(&mut self, idx: usize, pos: Vec3, depth: u32) {
6974        if self.children.is_some() {
6975            let child_idx = self.child_index(pos);
6976            if let Some(children) = &mut self.children {
6977                children[child_idx].insert(idx, pos, depth + 1);
6978            }
6979            return;
6980        }
6981        self.indices.push(idx);
6982        if self.indices.len() > Self::MAX_CAPACITY && depth < Self::MAX_DEPTH {
6983            self.subdivide(depth);
6984        }
6985    }
6986
6987    fn child_index(&self, p: Vec3) -> usize {
6988        let dx = if p.x >= self.center.x { 1 } else { 0 };
6989        let dy = if p.y >= self.center.y { 2 } else { 0 };
6990        let dz = if p.z >= self.center.z { 4 } else { 0 };
6991        dx | dy | dz
6992    }
6993
6994    fn subdivide(&mut self, depth: u32) {
6995        let h = self.half * 0.5;
6996        let c = self.center;
6997        let make_child = |dx: f32, dy: f32, dz: f32| {
6998            OctreeNode::new(c + Vec3::new(dx * h, dy * h, dz * h), h)
6999        };
7000        let children: [OctreeNode; 8] = [
7001            make_child(-1.0, -1.0, -1.0),
7002            make_child( 1.0, -1.0, -1.0),
7003            make_child(-1.0,  1.0, -1.0),
7004            make_child( 1.0,  1.0, -1.0),
7005            make_child(-1.0, -1.0,  1.0),
7006            make_child( 1.0, -1.0,  1.0),
7007            make_child(-1.0,  1.0,  1.0),
7008            make_child( 1.0,  1.0,  1.0),
7009        ];
7010        self.children = Some(Box::new(children));
7011        let old_indices: Vec<usize> = self.indices.drain(..).collect();
7012        // Re-insert requires positions; store indices in parent for now
7013        // (actual re-insert needs positions from outside — skip for leaf storage)
7014        self.indices = old_indices;
7015    }
7016
7017    pub fn query_sphere(&self, center: Vec3, radius: f32, result: &mut Vec<usize>) {
7018        let d = center - self.center;
7019        let max_d = d.x.abs().max(d.y.abs()).max(d.z.abs());
7020        if max_d > self.half + radius { return; }
7021        for &i in &self.indices {
7022            result.push(i);
7023        }
7024        if let Some(children) = &self.children {
7025            for child in children.iter() {
7026                child.query_sphere(center, radius, result);
7027            }
7028        }
7029    }
7030
7031    pub fn count(&self) -> usize {
7032        let mut n = self.indices.len();
7033        if let Some(children) = &self.children {
7034            for c in children.iter() { n += c.count(); }
7035        }
7036        n
7037    }
7038}
7039
7040pub struct Octree {
7041    pub root:      OctreeNode,
7042    pub positions: Vec<Vec3>,
7043}
7044
7045impl Octree {
7046    pub fn build(positions: &[Vec3]) -> Self {
7047        let mut min = Vec3::splat(f32::MAX);
7048        let mut max = Vec3::splat(f32::MIN);
7049        for &p in positions {
7050            min = min.min(p);
7051            max = max.max(p);
7052        }
7053        let center = (min + max) * 0.5;
7054        let half   = ((max - min).max_element() * 0.5 + 0.001).max(1.0);
7055        let mut root = OctreeNode::new(center, half);
7056        for (i, &p) in positions.iter().enumerate() {
7057            root.insert(i, p, 0);
7058        }
7059        Self { root, positions: positions.to_vec() }
7060    }
7061
7062    pub fn radius_search(&self, center: Vec3, radius: f32) -> Vec<usize> {
7063        let mut candidates = Vec::new();
7064        self.root.query_sphere(center, radius, &mut candidates);
7065        candidates.sort_unstable();
7066        candidates.dedup();
7067        let r2 = radius * radius;
7068        candidates.into_iter()
7069            .filter(|&i| i < self.positions.len() && (self.positions[i] - center).length_squared() <= r2)
7070            .collect()
7071    }
7072
7073    pub fn nearest(&self, query: Vec3, k: usize) -> Vec<usize> {
7074        if self.positions.is_empty() { return Vec::new(); }
7075        // Brute-force for small sets; for large octrees, use radius expansion
7076        let mut dists: Vec<(f32, usize)> = self.positions.iter().enumerate()
7077            .map(|(i, &p)| ((p - query).length_squared(), i))
7078            .collect();
7079        dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
7080        dists.into_iter().take(k).map(|(_, i)| i).collect()
7081    }
7082}
7083
7084// ──────────────────────────────────────────────────────────────────────────────
7085// SECTION 9: Mesh Boolean Operations (particle-based CSG)
7086// ──────────────────────────────────────────────────────────────────────────────
7087
7088#[derive(Clone, Debug)]
7089pub enum CsgOperation {
7090    Union,
7091    Subtract,
7092    Intersect,
7093    Difference,
7094}
7095
7096pub struct ParticleCsg;
7097
7098impl ParticleCsg {
7099    /// Union: merge two models, removing overlapping particles
7100    pub fn union(a: &ParticleModel, b: &ParticleModel, merge_threshold: f32) -> ParticleModel {
7101        let mut result = a.clone();
7102        let thresh2 = merge_threshold * merge_threshold;
7103        let a_positions: Vec<Vec3> = a.particles.iter().map(|p| p.position).collect();
7104        'outer: for bp in &b.particles {
7105            for ap in &a_positions {
7106                if (*ap - bp.position).length_squared() < thresh2 {
7107                    continue 'outer;
7108                }
7109            }
7110            result.particles.push(bp.clone());
7111        }
7112        result.recompute_bounds();
7113        result
7114    }
7115
7116    /// Subtract: keep particles from A that are not inside B's bounding volume
7117    pub fn subtract(a: &ParticleModel, b: &ParticleModel, margin: f32) -> ParticleModel {
7118        let b_bounds = &b.bounds;
7119        let expanded_min = b_bounds.min - Vec3::splat(margin);
7120        let expanded_max = b_bounds.max + Vec3::splat(margin);
7121        let mut result = a.clone();
7122        result.particles.retain(|p| {
7123            let inside = p.position.x >= expanded_min.x && p.position.x <= expanded_max.x
7124                && p.position.y >= expanded_min.y && p.position.y <= expanded_max.y
7125                && p.position.z >= expanded_min.z && p.position.z <= expanded_max.z;
7126            !inside
7127        });
7128        result.recompute_bounds();
7129        result
7130    }
7131
7132    /// Intersect: keep only particles from A that overlap with B's volume
7133    pub fn intersect(a: &ParticleModel, b: &ParticleModel, margin: f32) -> ParticleModel {
7134        let b_bounds = &b.bounds;
7135        let expanded_min = b_bounds.min - Vec3::splat(margin);
7136        let expanded_max = b_bounds.max + Vec3::splat(margin);
7137        let mut result = a.clone();
7138        result.particles.retain(|p| {
7139            p.position.x >= expanded_min.x && p.position.x <= expanded_max.x
7140                && p.position.y >= expanded_min.y && p.position.y <= expanded_max.y
7141                && p.position.z >= expanded_min.z && p.position.z <= expanded_max.z
7142        });
7143        result.recompute_bounds();
7144        result
7145    }
7146
7147    /// Shell: keep particles near the surface of B (within threshold)
7148    pub fn shell(model: &ParticleModel, b: &ParticleModel, shell_thickness: f32) -> ParticleModel {
7149        let b_positions: Vec<Vec3> = b.particles.iter().map(|p| p.position).collect();
7150        let t2 = shell_thickness * shell_thickness;
7151        let mut result = model.clone();
7152        result.particles.retain(|p| {
7153            b_positions.iter().any(|&bp| (bp - p.position).length_squared() <= t2)
7154        });
7155        result.recompute_bounds();
7156        result
7157    }
7158
7159    /// Apply operation
7160    pub fn apply(op: &CsgOperation, a: &ParticleModel, b: &ParticleModel, threshold: f32) -> ParticleModel {
7161        match op {
7162            CsgOperation::Union     => Self::union(a, b, threshold),
7163            CsgOperation::Subtract  => Self::subtract(a, b, threshold),
7164            CsgOperation::Intersect => Self::intersect(a, b, threshold),
7165            CsgOperation::Difference => {
7166                // symmetric difference: union - intersect
7167                let u = Self::union(a, b, threshold);
7168                let i = Self::intersect(a, b, threshold);
7169                Self::subtract(&u, &i, threshold * 0.5)
7170            }
7171        }
7172    }
7173}
7174
7175// ──────────────────────────────────────────────────────────────────────────────
7176// SECTION 10: Color Gradient and Palette Tools
7177// ──────────────────────────────────────────────────────────────────────────────
7178
7179#[derive(Clone, Debug)]
7180pub struct GradientStop {
7181    pub t:     f32,
7182    pub color: Vec4,
7183}
7184
7185#[derive(Clone, Debug)]
7186pub struct ColorGradient {
7187    pub stops: Vec<GradientStop>,
7188    pub name:  String,
7189}
7190
7191impl ColorGradient {
7192    pub fn new(name: &str) -> Self {
7193        Self { stops: Vec::new(), name: name.to_string() }
7194    }
7195
7196    pub fn add_stop(&mut self, t: f32, color: Vec4) {
7197        self.stops.push(GradientStop { t, color });
7198        self.stops.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
7199    }
7200
7201    pub fn evaluate(&self, t: f32) -> Vec4 {
7202        if self.stops.is_empty() { return Vec4::ONE; }
7203        if self.stops.len() == 1 { return self.stops[0].color; }
7204        let t = t.clamp(0.0, 1.0);
7205        if t <= self.stops[0].t { return self.stops[0].color; }
7206        if t >= self.stops.last().unwrap().t { return self.stops.last().unwrap().color; }
7207        for i in 1..self.stops.len() {
7208            if t <= self.stops[i].t {
7209                let a = &self.stops[i - 1];
7210                let b = &self.stops[i];
7211                let local_t = (t - a.t) / (b.t - a.t).max(1e-6);
7212                return a.color.lerp(b.color, local_t);
7213            }
7214        }
7215        self.stops.last().unwrap().color
7216    }
7217
7218    pub fn rainbow() -> Self {
7219        let mut g = Self::new("rainbow");
7220        g.add_stop(0.0,  Vec4::new(1.0, 0.0, 0.0, 1.0));
7221        g.add_stop(0.16, Vec4::new(1.0, 0.5, 0.0, 1.0));
7222        g.add_stop(0.33, Vec4::new(1.0, 1.0, 0.0, 1.0));
7223        g.add_stop(0.5,  Vec4::new(0.0, 1.0, 0.0, 1.0));
7224        g.add_stop(0.66, Vec4::new(0.0, 0.5, 1.0, 1.0));
7225        g.add_stop(0.83, Vec4::new(0.0, 0.0, 1.0, 1.0));
7226        g.add_stop(1.0,  Vec4::new(0.5, 0.0, 1.0, 1.0));
7227        g
7228    }
7229
7230    pub fn grayscale() -> Self {
7231        let mut g = Self::new("grayscale");
7232        g.add_stop(0.0, Vec4::new(0.0, 0.0, 0.0, 1.0));
7233        g.add_stop(1.0, Vec4::new(1.0, 1.0, 1.0, 1.0));
7234        g
7235    }
7236
7237    pub fn fire() -> Self {
7238        let mut g = Self::new("fire");
7239        g.add_stop(0.0,  Vec4::new(0.0, 0.0, 0.0, 1.0));
7240        g.add_stop(0.25, Vec4::new(0.5, 0.0, 0.0, 1.0));
7241        g.add_stop(0.5,  Vec4::new(1.0, 0.3, 0.0, 1.0));
7242        g.add_stop(0.75, Vec4::new(1.0, 0.8, 0.0, 1.0));
7243        g.add_stop(1.0,  Vec4::new(1.0, 1.0, 0.9, 1.0));
7244        g
7245    }
7246
7247    pub fn apply_height(&self, model: &mut ParticleModel) {
7248        if model.particles.is_empty() { return; }
7249        let min_y = model.particles.iter().map(|p| p.position.y).fold(f32::MAX, f32::min);
7250        let max_y = model.particles.iter().map(|p| p.position.y).fold(f32::MIN, f32::max);
7251        let range = (max_y - min_y).max(1e-6);
7252        for p in &mut model.particles {
7253            let t = (p.position.y - min_y) / range;
7254            p.color = self.evaluate(t);
7255        }
7256    }
7257
7258    pub fn apply_distance(&self, model: &mut ParticleModel, origin: Vec3, max_dist: f32) {
7259        for p in &mut model.particles {
7260            let d = (p.position - origin).length() / max_dist.max(1e-6);
7261            p.color = self.evaluate(d.clamp(0.0, 1.0));
7262        }
7263    }
7264
7265    pub fn apply_normal_angle(&self, model: &mut ParticleModel, reference: Vec3) {
7266        let ref_n = reference.normalize_or_zero();
7267        for p in &mut model.particles {
7268            let angle = ref_n.dot(p.normal.normalize_or_zero()).clamp(-1.0, 1.0).acos();
7269            let t = angle / std::f32::consts::PI;
7270            p.color = self.evaluate(t);
7271        }
7272    }
7273
7274    pub fn sample_n(&self, n: usize) -> Vec<Vec4> {
7275        (0..n).map(|i| self.evaluate(i as f32 / (n - 1).max(1) as f32)).collect()
7276    }
7277}
7278
7279pub struct PaletteManager {
7280    pub gradients: Vec<ColorGradient>,
7281    pub palettes:  HashMap<String, Vec<Vec4>>,
7282}
7283
7284impl PaletteManager {
7285    pub fn new() -> Self {
7286        let mut pm = Self { gradients: Vec::new(), palettes: HashMap::new() };
7287        pm.gradients.push(ColorGradient::rainbow());
7288        pm.gradients.push(ColorGradient::grayscale());
7289        pm.gradients.push(ColorGradient::fire());
7290        pm
7291    }
7292
7293    pub fn add_gradient(&mut self, g: ColorGradient) { self.gradients.push(g); }
7294    pub fn add_palette(&mut self, name: &str, colors: Vec<Vec4>) {
7295        self.palettes.insert(name.to_string(), colors);
7296    }
7297    pub fn get_gradient(&self, name: &str) -> Option<&ColorGradient> {
7298        self.gradients.iter().find(|g| g.name == name)
7299    }
7300    pub fn get_palette(&self, name: &str) -> Option<&Vec<Vec4>> {
7301        self.palettes.get(name)
7302    }
7303
7304    pub fn quantize_model(&self, model: &mut ParticleModel, palette_name: &str) {
7305        let Some(palette) = self.palettes.get(palette_name) else { return; };
7306        if palette.is_empty() { return; }
7307        for p in &mut model.particles {
7308            let best = palette.iter()
7309                .min_by(|a, b| {
7310                    (**a - p.color).length_squared()
7311                        .partial_cmp(&(**b - p.color).length_squared())
7312                        .unwrap_or(std::cmp::Ordering::Equal)
7313                })
7314                .copied()
7315                .unwrap_or(p.color);
7316            p.color = best;
7317        }
7318    }
7319}
7320
7321// ──────────────────────────────────────────────────────────────────────────────
7322// SECTION 11: Particle Decals and Overlays
7323// ──────────────────────────────────────────────────────────────────────────────
7324
7325#[derive(Clone, Debug)]
7326pub struct ParticleDecal {
7327    pub position:   Vec3,
7328    pub normal:     Vec3,
7329    pub radius:     f32,
7330    pub depth:      f32,
7331    pub char_set:   Vec<char>,
7332    pub color:      Vec4,
7333    pub blend_mode: DecalBlend,
7334    pub opacity:    f32,
7335}
7336
7337#[derive(Clone, Debug)]
7338pub enum DecalBlend {
7339    Replace,
7340    Multiply,
7341    Add,
7342    Screen,
7343    Overlay,
7344}
7345
7346impl ParticleDecal {
7347    pub fn new(position: Vec3, normal: Vec3, radius: f32) -> Self {
7348        Self {
7349            position,
7350            normal:     normal.normalize_or_zero(),
7351            radius,
7352            depth:      0.1,
7353            char_set:   vec!['#'],
7354            color:      Vec4::ONE,
7355            blend_mode: DecalBlend::Replace,
7356            opacity:    1.0,
7357        }
7358    }
7359
7360    pub fn apply_to_model(&self, model: &mut ParticleModel) {
7361        let r2 = self.radius * self.radius;
7362        for p in &mut model.particles {
7363            let d = p.position - self.position;
7364            let dist2 = d.length_squared();
7365            if dist2 > r2 { continue; }
7366            // Project onto decal plane
7367            let along_normal = d.dot(self.normal);
7368            if along_normal.abs() > self.depth { continue; }
7369            let t = 1.0 - (dist2 / r2).sqrt();
7370            let alpha = t * self.opacity;
7371            // Choose char
7372            if !self.char_set.is_empty() {
7373                let idx = ((1.0 - t) * (self.char_set.len() - 1) as f32) as usize;
7374                let idx = idx.min(self.char_set.len() - 1);
7375                p.character = self.char_set[idx];
7376            }
7377            // Blend color
7378            p.color = match &self.blend_mode {
7379                DecalBlend::Replace  => self.color.lerp(p.color, 1.0 - alpha),
7380                DecalBlend::Multiply => {
7381                    let m = Vec4::new(p.color.x * self.color.x, p.color.y * self.color.y,
7382                                      p.color.z * self.color.z, p.color.w);
7383                    p.color.lerp(m, alpha)
7384                }
7385                DecalBlend::Add => {
7386                    Vec4::new(
7387                        (p.color.x + self.color.x * alpha).min(1.0),
7388                        (p.color.y + self.color.y * alpha).min(1.0),
7389                        (p.color.z + self.color.z * alpha).min(1.0),
7390                        p.color.w,
7391                    )
7392                }
7393                DecalBlend::Screen => {
7394                    let sc = Vec4::new(
7395                        1.0 - (1.0 - p.color.x) * (1.0 - self.color.x),
7396                        1.0 - (1.0 - p.color.y) * (1.0 - self.color.y),
7397                        1.0 - (1.0 - p.color.z) * (1.0 - self.color.z),
7398                        p.color.w,
7399                    );
7400                    p.color.lerp(sc, alpha)
7401                }
7402                DecalBlend::Overlay => {
7403                    let overlay = |base: f32, src: f32| {
7404                        if base < 0.5 { 2.0 * base * src } else { 1.0 - 2.0 * (1.0 - base) * (1.0 - src) }
7405                    };
7406                    let ov = Vec4::new(
7407                        overlay(p.color.x, self.color.x),
7408                        overlay(p.color.y, self.color.y),
7409                        overlay(p.color.z, self.color.z),
7410                        p.color.w,
7411                    );
7412                    p.color.lerp(ov, alpha)
7413                }
7414            };
7415        }
7416    }
7417}
7418
7419pub struct DecalLayer {
7420    pub decals: Vec<ParticleDecal>,
7421    pub enabled: bool,
7422}
7423
7424impl DecalLayer {
7425    pub fn new() -> Self { Self { decals: Vec::new(), enabled: true } }
7426    pub fn add(&mut self, d: ParticleDecal) { self.decals.push(d); }
7427    pub fn apply_all(&self, model: &mut ParticleModel) {
7428        if !self.enabled { return; }
7429        for d in &self.decals { d.apply_to_model(model); }
7430    }
7431    pub fn clear(&mut self) { self.decals.clear(); }
7432}
7433
7434// ──────────────────────────────────────────────────────────────────────────────
7435// SECTION 12: Instancing and Scatter
7436// ──────────────────────────────────────────────────────────────────────────────
7437
7438#[derive(Clone, Debug)]
7439pub struct ScatterInstance {
7440    pub position:  Vec3,
7441    pub rotation:  Quat,
7442    pub scale:     Vec3,
7443    pub color_tint: Vec4,
7444}
7445
7446#[derive(Clone, Debug)]
7447pub struct ScatterSettings {
7448    pub density:       f32,
7449    pub random_rot:    bool,
7450    pub align_normal:  bool,
7451    pub scale_min:     f32,
7452    pub scale_max:     f32,
7453    pub color_var:     f32,
7454    pub seed:          u64,
7455}
7456
7457impl Default for ScatterSettings {
7458    fn default() -> Self {
7459        Self {
7460            density:      1.0,
7461            random_rot:   true,
7462            align_normal: true,
7463            scale_min:    0.8,
7464            scale_max:    1.2,
7465            color_var:    0.1,
7466            seed:         42,
7467        }
7468    }
7469}
7470
7471pub struct ParticleScatter {
7472    pub template:  ParticleModel,
7473    pub instances: Vec<ScatterInstance>,
7474    pub settings:  ScatterSettings,
7475}
7476
7477impl ParticleScatter {
7478    pub fn new(template: ParticleModel) -> Self {
7479        Self { template, instances: Vec::new(), settings: ScatterSettings::default() }
7480    }
7481
7482    pub fn scatter_on_model(&mut self, surface: &ParticleModel) {
7483        self.instances.clear();
7484        let mut rng = self.settings.seed;
7485        let count = (surface.particles.len() as f32 * self.settings.density) as usize;
7486        for i in 0..count {
7487            if i >= surface.particles.len() { break; }
7488            let surf_p = &surface.particles[i];
7489            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
7490            let r0 = (rng >> 33) as f32 / u32::MAX as f32;
7491            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
7492            let r1 = (rng >> 33) as f32 / u32::MAX as f32;
7493            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
7494            let r2 = (rng >> 33) as f32 / u32::MAX as f32;
7495            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
7496            let r3 = (rng >> 33) as f32 / u32::MAX as f32;
7497            let scale_s = self.settings.scale_min + r0 * (self.settings.scale_max - self.settings.scale_min);
7498            let rotation = if self.settings.align_normal {
7499                let up = Vec3::Y;
7500                let n  = surf_p.normal.normalize_or_zero();
7501                let axis = up.cross(n);
7502                if axis.length_squared() > 1e-6 {
7503                    Quat::from_axis_angle(axis.normalize(), up.dot(n).clamp(-1.0, 1.0).acos())
7504                } else { Quat::IDENTITY }
7505            } else if self.settings.random_rot {
7506                // Random quaternion from uniform distribution
7507                let u = r1; let v = r2; let w = r3;
7508                Quat::from_xyzw(
7509                    (1.0 - u).sqrt() * (2.0 * std::f32::consts::PI * v).sin(),
7510                    (1.0 - u).sqrt() * (2.0 * std::f32::consts::PI * v).cos(),
7511                    u.sqrt() * (2.0 * std::f32::consts::PI * w).sin(),
7512                    u.sqrt() * (2.0 * std::f32::consts::PI * w).cos(),
7513                )
7514            } else { Quat::IDENTITY };
7515            let color_tint = Vec4::new(
7516                1.0 + (r0 - 0.5) * self.settings.color_var,
7517                1.0 + (r1 - 0.5) * self.settings.color_var,
7518                1.0 + (r2 - 0.5) * self.settings.color_var,
7519                1.0,
7520            );
7521            self.instances.push(ScatterInstance {
7522                position: surf_p.position,
7523                rotation,
7524                scale: Vec3::splat(scale_s),
7525                color_tint,
7526            });
7527        }
7528    }
7529
7530    pub fn bake(&self) -> ParticleModel {
7531        let mut result = ParticleModel::new(0, "scatter_baked");
7532        for inst in &self.instances {
7533            let xform = Mat4::from_scale_rotation_translation(inst.scale, inst.rotation, inst.position);
7534            for tp in &self.template.particles {
7535                let new_pos = (xform * Vec4::new(tp.position.x, tp.position.y, tp.position.z, 1.0)).truncate();
7536                let new_nrm = (inst.rotation * tp.normal).normalize_or_zero();
7537                result.particles.push(ModelParticle {
7538                    position:     new_pos,
7539                    character:    tp.character,
7540                    color:        Vec4::new(
7541                                      tp.color.x * inst.color_tint.x,
7542                                      tp.color.y * inst.color_tint.y,
7543                                      tp.color.z * inst.color_tint.z,
7544                                      tp.color.w,
7545                                  ),
7546                    emission:     tp.emission,
7547                    normal:       new_nrm,
7548                    bone_weights: tp.bone_weights,
7549                    bone_indices: tp.bone_indices,
7550                    group_id:     tp.group_id,
7551                    layer_id:     tp.layer_id,
7552                    selected:     false,
7553                    locked:       false,
7554                });
7555            }
7556        }
7557        result.recompute_bounds();
7558        result
7559    }
7560}
7561
7562// ──────────────────────────────────────────────────────────────────────────────
7563// SECTION 13: History/Journal with branching
7564// ──────────────────────────────────────────────────────────────────────────────
7565
7566#[derive(Clone, Debug)]
7567pub struct HistoryNode {
7568    pub id:       u64,
7569    pub parent:   Option<u64>,
7570    pub children: Vec<u64>,
7571    pub snapshot: ModelSnapshot,
7572    pub label:    String,
7573    pub timestamp: u64,
7574}
7575
7576pub struct BranchingHistory {
7577    pub nodes:      HashMap<u64, HistoryNode>,
7578    pub current_id: Option<u64>,
7579    pub next_id:    u64,
7580}
7581
7582impl BranchingHistory {
7583    pub fn new() -> Self {
7584        Self { nodes: HashMap::new(), current_id: None, next_id: 1 }
7585    }
7586
7587    pub fn push(&mut self, snapshot: ModelSnapshot, label: &str) -> u64 {
7588        let id = self.next_id;
7589        self.next_id += 1;
7590        if let Some(parent_id) = self.current_id {
7591            if let Some(parent) = self.nodes.get_mut(&parent_id) {
7592                parent.children.push(id);
7593            }
7594        }
7595        self.nodes.insert(id, HistoryNode {
7596            id,
7597            parent:    self.current_id,
7598            children:  Vec::new(),
7599            snapshot,
7600            label:     label.to_string(),
7601            timestamp: id, // monotonic surrogate
7602        });
7603        self.current_id = Some(id);
7604        id
7605    }
7606
7607    pub fn undo(&mut self) -> Option<&ModelSnapshot> {
7608        let cur = self.current_id?;
7609        let parent = self.nodes.get(&cur)?.parent?;
7610        self.current_id = Some(parent);
7611        Some(&self.nodes[&parent].snapshot)
7612    }
7613
7614    pub fn redo_to(&mut self, child_id: u64) -> Option<&ModelSnapshot> {
7615        let cur = self.current_id?;
7616        if !self.nodes.get(&cur)?.children.contains(&child_id) { return None; }
7617        self.current_id = Some(child_id);
7618        Some(&self.nodes[&child_id].snapshot)
7619    }
7620
7621    pub fn list_children(&self) -> Vec<(u64, &str)> {
7622        let Some(cur) = self.current_id else { return Vec::new(); };
7623        let Some(node) = self.nodes.get(&cur) else { return Vec::new(); };
7624        node.children.iter()
7625            .filter_map(|&id| self.nodes.get(&id).map(|n| (id, n.label.as_str())))
7626            .collect()
7627    }
7628
7629    pub fn path_to_root(&self) -> Vec<u64> {
7630        let mut path = Vec::new();
7631        let mut cur = self.current_id;
7632        while let Some(id) = cur {
7633            path.push(id);
7634            cur = self.nodes.get(&id).and_then(|n| n.parent);
7635        }
7636        path
7637    }
7638
7639    pub fn branch_count(&self) -> usize {
7640        self.nodes.values().filter(|n| n.children.len() > 1).count()
7641    }
7642}
7643
7644// ──────────────────────────────────────────────────────────────────────────────
7645// SECTION 14: Final integration tests for ext3
7646// ──────────────────────────────────────────────────────────────────────────────
7647
7648#[cfg(test)]
7649mod ext3_tests {
7650    use super::*;
7651
7652    fn make_sphere_model(n: usize) -> ParticleModel {
7653        let mut m = ParticleModel::new(1, "sphere");
7654        let golden = std::f32::consts::PI * (3.0 - 5.0_f32.sqrt());
7655        for i in 0..n {
7656            let y = 1.0 - (i as f32 / (n - 1).max(1) as f32) * 2.0;
7657            let r = (1.0 - y * y).max(0.0).sqrt();
7658            let theta = golden * i as f32;
7659            m.particles.push(ModelParticle {
7660                position: Vec3::new(r * theta.cos(), y, r * theta.sin()),
7661                character: 'o',
7662                color: Vec4::new(0.8, 0.6, 0.4, 1.0),
7663                emission: 0.0,
7664                normal: Vec3::new(r * theta.cos(), y, r * theta.sin()).normalize_or_zero(),
7665                bone_weights: [1.0, 0.0, 0.0, 0.0],
7666                bone_indices: [0, 0, 0, 0],
7667                group_id: 0,
7668                layer_id: 0,
7669                selected: false,
7670                locked: false,
7671            });
7672        }
7673        m.recompute_bounds();
7674        m
7675    }
7676
7677    #[test]
7678    fn test_constraint_plane() {
7679        let c = ParticleConstraint::new(0, ConstraintKind::Plane {
7680            normal: Vec3::Y, offset: 0.0
7681        });
7682        let pos = Vec3::new(1.0, -2.0, 0.0);
7683        let result = c.apply(pos);
7684        assert!(result.y.abs() < 0.001, "plane constraint should bring y to 0");
7685    }
7686
7687    #[test]
7688    fn test_constraint_cage() {
7689        let c = ParticleConstraint::new(0, ConstraintKind::Cage {
7690            min: Vec3::splat(-1.0), max: Vec3::splat(1.0)
7691        });
7692        let pos = Vec3::new(5.0, -3.0, 2.0);
7693        let r = c.apply(pos);
7694        assert!(r.x <= 1.0 && r.x >= -1.0);
7695        assert!(r.y <= 1.0 && r.y >= -1.0);
7696        assert!(r.z <= 1.0 && r.z >= -1.0);
7697    }
7698
7699    #[test]
7700    fn test_spring_simulation() {
7701        let mut sim = PhysicsSimulator::new();
7702        let a = sim.add_particle(Vec3::ZERO, 1.0);
7703        let b = sim.add_particle(Vec3::new(2.0, 0.0, 0.0), 1.0);
7704        sim.particles[a].fixed = true;
7705        sim.add_spring(a, b, 10.0);
7706        sim.gravity = Vec3::ZERO;
7707        let initial_pos = sim.particles[b].position;
7708        sim.step(0.016);
7709        // Spring should pull b toward a (rest length = 2.0 initially, so no force)
7710        let final_pos = sim.particles[b].position;
7711        let moved = (final_pos - initial_pos).length();
7712        assert!(moved < 0.1, "no displacement when at rest length: {}", moved);
7713    }
7714
7715    #[test]
7716    fn test_curve_polyline() {
7717        let mut curve = ModelCurve::new("test", CurveType::Polyline);
7718        curve.add_point(Vec3::ZERO);
7719        curve.add_point(Vec3::new(1.0, 0.0, 0.0));
7720        curve.add_point(Vec3::new(2.0, 0.0, 0.0));
7721        let mid = curve.evaluate(0.5);
7722        assert!((mid.x - 1.0).abs() < 0.01, "midpoint should be x=1: {}", mid.x);
7723    }
7724
7725    #[test]
7726    fn test_curve_bezier() {
7727        let mut curve = ModelCurve::new("bez", CurveType::Bezier);
7728        curve.add_point(Vec3::ZERO);
7729        curve.add_point(Vec3::new(0.0, 2.0, 0.0));
7730        curve.add_point(Vec3::new(1.0, 2.0, 0.0));
7731        curve.add_point(Vec3::new(1.0, 0.0, 0.0));
7732        let start = curve.evaluate(0.0);
7733        let end   = curve.evaluate(1.0);
7734        assert!(start.length() < 0.001);
7735        assert!((end - Vec3::new(1.0, 0.0, 0.0)).length() < 0.001);
7736    }
7737
7738    #[test]
7739    fn test_curve_arc_length() {
7740        let mut curve = ModelCurve::new("line", CurveType::Polyline);
7741        curve.add_point(Vec3::ZERO);
7742        curve.add_point(Vec3::new(10.0, 0.0, 0.0));
7743        let len = curve.arc_length(100);
7744        assert!((len - 10.0).abs() < 0.1, "arc length should be ~10: {}", len);
7745    }
7746
7747    #[test]
7748    fn test_texture_projection_spherical() {
7749        let proj = TextureProjector::new(ProjectionMode::Spherical { center: Vec3::ZERO });
7750        let uv = proj.project(Vec3::new(1.0, 0.0, 0.0));
7751        assert!(uv.x >= 0.0 && uv.x <= 1.0);
7752        assert!(uv.y >= 0.0 && uv.y <= 1.0);
7753    }
7754
7755    #[test]
7756    fn test_field_vortex() {
7757        let field = FieldType::Vortex {
7758            axis: Vec3::Y, origin: Vec3::ZERO, angular_vel: 2.0, decay: 0.5
7759        };
7760        let force = field.evaluate(Vec3::new(1.0, 0.0, 0.0), 0.0);
7761        // Should have a tangential component (non-zero)
7762        assert!(force.length() > 0.0);
7763    }
7764
7765    #[test]
7766    fn test_particle_field_displacement() {
7767        let mut field = ParticleField::new();
7768        field.add(FieldType::Wind { direction: Vec3::X, strength: 1.0, turbulence: 0.0 });
7769        let mut model = make_sphere_model(50);
7770        let before: Vec<Vec3> = model.particles.iter().map(|p| p.position).collect();
7771        field.apply_displacement(&mut model, 0.1, 1.0);
7772        let moved = model.particles.iter().zip(before.iter())
7773            .filter(|(a, b)| (a.position - **b).length() > 0.001)
7774            .count();
7775        assert!(moved > 0, "wind should move particles");
7776    }
7777
7778    #[test]
7779    fn test_render_buffer_depth() {
7780        let mut buf = RenderBuffer::new(80, 24);
7781        let cell_close = RenderCell { character: 'X', depth: 1.0, ..RenderCell::default() };
7782        let cell_far   = RenderCell { character: 'Y', depth: 5.0, ..RenderCell::default() };
7783        buf.set(10, 10, cell_far.clone());
7784        buf.set(10, 10, cell_close.clone());
7785        assert_eq!(buf.get(10, 10).unwrap().character, 'X', "closer should win");
7786        buf.set(10, 10, RenderCell { character: 'Z', depth: 10.0, ..RenderCell::default() });
7787        assert_eq!(buf.get(10, 10).unwrap().character, 'X', "closer should still win");
7788    }
7789
7790    #[test]
7791    fn test_particle_diff_round_trip() {
7792        let before = make_sphere_model(30);
7793        let mut after = before.clone();
7794        after.particles[0].position += Vec3::new(1.0, 0.0, 0.0);
7795        after.particles[5].color    = Vec4::new(1.0, 0.0, 0.0, 1.0);
7796        let diff = ParticleDiff::compute(&before, &after);
7797        assert_eq!(diff.moved.len(), 1);
7798        assert_eq!(diff.recolored.len(), 1);
7799        let inv = diff.invert();
7800        let mut restored = after.clone();
7801        inv.apply(&mut restored);
7802        let d = (restored.particles[0].position - before.particles[0].position).length();
7803        assert!(d < 0.001, "position should be restored: {}", d);
7804    }
7805
7806    #[test]
7807    fn test_octree_radius_search() {
7808        let positions: Vec<Vec3> = (0..100).map(|i| {
7809            let t = i as f32 * 0.1;
7810            Vec3::new(t.sin(), t.cos(), t * 0.1)
7811        }).collect();
7812        let tree = Octree::build(&positions);
7813        let near = tree.radius_search(Vec3::ZERO, 1.5);
7814        assert!(!near.is_empty(), "should find neighbors");
7815        for &i in &near {
7816            assert!(i < positions.len());
7817            assert!(positions[i].length() <= 1.5 + 1e-4);
7818        }
7819    }
7820
7821    #[test]
7822    fn test_csg_union() {
7823        let a = make_sphere_model(50);
7824        let mut b = make_sphere_model(20);
7825        for p in &mut b.particles { p.position += Vec3::new(5.0, 0.0, 0.0); }
7826        b.recompute_bounds();
7827        let u = ParticleCsg::union(&a, &b, 0.05);
7828        assert_eq!(u.particles.len(), 70, "union should have all particles");
7829    }
7830
7831    #[test]
7832    fn test_csg_subtract() {
7833        let mut a = make_sphere_model(100);
7834        // center a at origin
7835        let b = make_sphere_model(10); // small sphere at origin
7836        let s = ParticleCsg::subtract(&a, &b, 0.0);
7837        assert!(s.particles.len() < a.particles.len(), "subtract should remove some");
7838    }
7839
7840    #[test]
7841    fn test_color_gradient() {
7842        let g = ColorGradient::rainbow();
7843        let c0 = g.evaluate(0.0);
7844        let c1 = g.evaluate(1.0);
7845        assert!((c0.x - 1.0).abs() < 0.01, "start should be red");
7846        assert!(c1.z > 0.0, "end should have blue");
7847        let samples = g.sample_n(10);
7848        assert_eq!(samples.len(), 10);
7849    }
7850
7851    #[test]
7852    fn test_gradient_apply_height() {
7853        let mut model = make_sphere_model(50);
7854        let g = ColorGradient::fire();
7855        g.apply_height(&mut model);
7856        // Just check it doesn't panic and colors are valid
7857        for p in &model.particles {
7858            assert!(p.color.x >= 0.0 && p.color.x <= 1.0);
7859            assert!(p.color.y >= 0.0 && p.color.y <= 1.0);
7860            assert!(p.color.z >= 0.0 && p.color.z <= 1.0);
7861        }
7862    }
7863
7864    #[test]
7865    fn test_decal_replace() {
7866        let mut model = make_sphere_model(50);
7867        let decal = ParticleDecal {
7868            position:   Vec3::new(1.0, 0.0, 0.0),
7869            normal:     Vec3::X,
7870            radius:     0.5,
7871            depth:      0.2,
7872            char_set:   vec!['@'],
7873            color:      Vec4::new(1.0, 0.0, 0.0, 1.0),
7874            blend_mode: DecalBlend::Replace,
7875            opacity:    1.0,
7876        };
7877        decal.apply_to_model(&mut model);
7878        // At least some particles should be affected
7879        let changed = model.particles.iter().filter(|p| p.character == '@').count();
7880        // May be 0 if no particles are exactly in range — just test no panic
7881        let _ = changed;
7882    }
7883
7884    #[test]
7885    fn test_scatter_bake() {
7886        let template = make_sphere_model(5);
7887        let surface  = make_sphere_model(20);
7888        let mut scatter = ParticleScatter::new(template);
7889        scatter.settings.density = 0.5;
7890        scatter.scatter_on_model(&surface);
7891        let baked = scatter.bake();
7892        assert!(!baked.particles.is_empty(), "baked model should have particles");
7893    }
7894
7895    #[test]
7896    fn test_branching_history() {
7897        let mut hist = BranchingHistory::new();
7898        let m0 = ParticleModel::new(1, "v0");
7899        let m1 = ParticleModel::new(2, "v1");
7900        let m2 = ParticleModel::new(3, "v2");
7901        hist.push(ModelSnapshot::capture(&m0, "v0"), "initial");
7902        hist.push(ModelSnapshot::capture(&m1, "v1"), "step1");
7903        let _snap = hist.undo();
7904        hist.push(ModelSnapshot::capture(&m2, "v2"), "branch");
7905        assert_eq!(hist.branch_count(), 1, "should have one branching point");
7906        // Current is the branch, whose parent is the root: two nodes.
7907        let path = hist.path_to_root();
7908        assert_eq!(path.len(), 2);
7909    }
7910
7911    #[test]
7912    fn test_camera_orbit() {
7913        let mut cam = RenderCamera::new();
7914        let initial_pos = cam.position;
7915        cam.orbit(0.1, 0.0);
7916        let new_pos = cam.position;
7917        let dist_before = (initial_pos - cam.target).length();
7918        let dist_after  = (new_pos    - cam.target).length();
7919        assert!((dist_before - dist_after).abs() < 0.01, "orbit preserves distance");
7920    }
7921
7922    #[test]
7923    fn test_camera_dolly() {
7924        let mut cam = RenderCamera::new();
7925        let d0 = (cam.position - cam.target).length();
7926        cam.dolly(-2.0);
7927        let d1 = (cam.position - cam.target).length();
7928        assert!(d1 > d0, "dolly backward increases distance");
7929    }
7930
7931    #[test]
7932    fn test_constraint_solver_multi() {
7933        let mut solver = ConstraintSolver::new();
7934        solver.add_constraint(ParticleConstraint::new(0, ConstraintKind::Cage {
7935            min: Vec3::splat(-1.0), max: Vec3::splat(1.0)
7936        }));
7937        solver.add_constraint(ParticleConstraint::new(1, ConstraintKind::Sphere {
7938            center: Vec3::ZERO, radius: 0.5
7939        }));
7940        let mut positions = vec![Vec3::new(10.0, 10.0, 10.0), Vec3::new(1.0, 0.0, 0.0)];
7941        solver.solve(&mut positions);
7942        assert!(positions[0].x <= 1.0);
7943        assert!(positions[1].length() <= 0.5 + 1e-4);
7944    }
7945
7946    #[test]
7947    fn test_frenet_frame() {
7948        let mut curve = ModelCurve::new("circle", CurveType::CatmullRom { alpha: 0.5 });
7949        for i in 0..8 {
7950            let a = i as f32 * std::f32::consts::TAU / 8.0;
7951            curve.add_point(Vec3::new(a.cos(), 0.0, a.sin()));
7952        }
7953        let (t, n, b) = curve.frenet_frame(0.5);
7954        // Tangent, normal, binormal should be roughly orthogonal
7955        assert!(t.dot(n).abs() < 0.1, "T perp N");
7956        assert!(t.dot(b).abs() < 0.1, "T perp B");
7957    }
7958
7959    #[test]
7960    fn test_nurbs_evaluation() {
7961        let mut curve = ModelCurve::new("nurbs", CurveType::Nurbs {
7962            degree:  3,
7963            weights: vec![1.0, 1.0, 1.0, 1.0],
7964        });
7965        curve.add_point(Vec3::ZERO);
7966        curve.add_point(Vec3::new(1.0, 1.0, 0.0));
7967        curve.add_point(Vec3::new(2.0, 1.0, 0.0));
7968        curve.add_point(Vec3::new(3.0, 0.0, 0.0));
7969        let p = curve.evaluate(0.5);
7970        assert!(p.x > 0.0 && p.x < 3.0, "NURBS midpoint in range: {:?}", p);
7971    }
7972}
7973
7974// SECTION 15: Utility functions and constants
7975
7976/// Remap clamped — like remap but clamps output
7977pub fn remap_clamped(value: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
7978    let t = ((value - in_min) / (in_max - in_min).max(1e-9)).clamp(0.0, 1.0);
7979    out_min + t * (out_max - out_min)
7980}
7981
7982/// Smoothstep with custom exponent
7983pub fn smoothstep_exp(edge0: f32, edge1: f32, x: f32, exp: f32) -> f32 {
7984    let t = ((x - edge0) / (edge1 - edge0).max(1e-9)).clamp(0.0, 1.0);
7985    t.powf(exp)
7986}
7987
7988/// Smootherstep variant with asymmetric rise/fall
7989pub fn asymmetric_smoothstep(edge0: f32, edge1: f32, x: f32, bias: f32) -> f32 {
7990    let t = ((x - edge0) / (edge1 - edge0).max(1e-9)).clamp(0.0, 1.0);
7991    let biased = if bias > 0.5 {
7992        1.0 - (1.0 - t).powf(1.0 / (1.0 - bias).max(0.01))
7993    } else {
7994        t.powf(1.0 / bias.max(0.01))
7995    };
7996    biased
7997}
7998
7999/// Signed angle between two Vec3s around an axis (degrees)
8000pub fn signed_angle_deg(from: Vec3, to: Vec3, axis: Vec3) -> f32 {
8001    let unsigned = from.dot(to).clamp(-1.0, 1.0).acos();
8002    let cross = from.cross(to);
8003    let signed = if axis.dot(cross) < 0.0 { -unsigned } else { unsigned };
8004    signed.to_degrees()
8005}
8006
8007/// Project vector onto a plane and normalize
8008pub fn project_onto_plane_normalized(v: Vec3, normal: Vec3) -> Vec3 {
8009    (v - normal * normal.dot(v)).normalize_or_zero()
8010}
8011
8012/// Reflect vector across a normal
8013pub fn reflect_vector(v: Vec3, normal: Vec3) -> Vec3 {
8014    v - normal * 2.0 * normal.dot(v)
8015}
8016
8017/// Rotate a Vec3 by angle around an axis
8018pub fn rotate_around_axis(v: Vec3, axis: Vec3, angle: f32) -> Vec3 {
8019    Quat::from_axis_angle(axis.normalize_or_zero(), angle) * v
8020}
8021
8022/// Convert RGB to HSL
8023pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
8024    let max = r.max(g).max(b);
8025    let min = r.min(g).min(b);
8026    let l = (max + min) * 0.5;
8027    if (max - min).abs() < 1e-6 { return (0.0, 0.0, l); }
8028    let d = max - min;
8029    let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
8030    let h = if max == r {
8031        (g - b) / d + if g < b { 6.0 } else { 0.0 }
8032    } else if max == g {
8033        (b - r) / d + 2.0
8034    } else {
8035        (r - g) / d + 4.0
8036    } / 6.0;
8037    (h, s, l)
8038}
8039
8040fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
8041    if t < 0.0 { t += 1.0; }
8042    if t > 1.0 { t -= 1.0; }
8043    if t < 1.0/6.0 { return p + (q - p) * 6.0 * t; }
8044    if t < 0.5     { return q; }
8045    if t < 2.0/3.0 { return p + (q - p) * (2.0/3.0 - t) * 6.0; }
8046    p
8047}
8048
8049/// Convert HSL to RGB
8050pub fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
8051    if s < 1e-6 { return (l, l, l); }
8052    let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
8053    let p = 2.0 * l - q;
8054    (hue_to_rgb(p, q, h + 1.0/3.0),
8055     hue_to_rgb(p, q, h),
8056     hue_to_rgb(p, q, h - 1.0/3.0))
8057}
8058
8059/// Random float in [0,1] from a LCG seed (mutates seed)
8060pub fn lcg_rand(seed: &mut u64) -> f32 {
8061    *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
8062    ((*seed >> 33) as u32) as f32 / u32::MAX as f32
8063}
8064
8065/// Halton sequence for quasi-random sampling
8066pub fn halton(index: u32, base: u32) -> f32 {
8067    let mut f = 1.0_f32;
8068    let mut r = 0.0_f32;
8069    let mut i = index;
8070    let b = base as f32;
8071    while i > 0 {
8072        f /= b;
8073        r += f * (i % base) as f32;
8074        i /= base;
8075    }
8076    r
8077}
8078
8079/// Map a 1D index to a 2D Hilbert curve coordinate (order n)
8080pub fn hilbert_d2xy(n: u32, d: u32) -> (u32, u32) {
8081    let mut s = 1u32;
8082    let mut x = 0u32;
8083    let mut y = 0u32;
8084    let mut t = d;
8085    while s < n {
8086        let rx = 1 & (t / 2);
8087        let ry = 1 & (t ^ rx);
8088        if ry == 0 {
8089            if rx == 1 { x = s.wrapping_sub(1).wrapping_sub(x); y = s.wrapping_sub(1).wrapping_sub(y); }
8090            std::mem::swap(&mut x, &mut y);
8091        }
8092        x = x.wrapping_add(s * rx);
8093        y = y.wrapping_add(s * ry);
8094        t /= 4;
8095        s *= 2;
8096    }
8097    (x, y)
8098}
8099
8100pub const GOLDEN_RATIO: f32 = 1.618033988749895;
8101pub const INV_GOLDEN_RATIO: f32 = 0.6180339887498948;
8102pub const SQRT3: f32 = 1.7320508075688772;