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
6pub 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#[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#[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 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#[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 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 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#[derive(Clone, Debug)]
372pub struct LodLevel {
373 pub level: u8,
374 pub particles: Vec<usize>, 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#[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#[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 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 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
578fn 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#[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#[derive(Clone, Debug, PartialEq)]
638pub enum FalloffCurve {
639 Constant,
640 Linear,
641 Smooth,
642 Sphere,
643 Root,
644 Sharp,
645}
646
647impl FalloffCurve {
648 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#[derive(Clone, Debug, PartialEq)]
679pub enum SymmetryMode {
680 None,
681 X, Y, Z,
682 XY, XZ, YZ,
683 XYZ,
684}
685
686impl SymmetryMode {
687 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#[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 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 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 ¤t {
822 if (particles[sel].position - p.position).length_squared() <= r2 {
823 self.selected.insert(i);
824 break;
825 }
826 }
827 }
828 }
829
830 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
859pub 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
904pub struct PrimitiveBuilder;
909
910impl PrimitiveBuilder {
911 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 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 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 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 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 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 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 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 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 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 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 particles.push(ModelParticle::new(
1142 Vec3::new(x, y, origin.z),
1143 character, color,
1144 ).with_normal(-Vec3::Z));
1145 particles.push(ModelParticle::new(
1147 Vec3::new(x, y, origin.z + depth),
1148 character, color,
1149 ).with_normal(Vec3::Z));
1150 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 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 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 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 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 let edge_mask = MC_EDGE_TABLE[case_idx as usize];
1225 let mut edge_verts: [Vec3; 12] = [Vec3::ZERO; 12];
1226
1227 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 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 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
1286pub fn simple_noise_2d(x: f32, y: f32) -> f32 {
1291 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
1313fn 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
1363static 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
1403static MC_TRI_TABLE: [[u8; 16]; 256] = {
1406 let mut t = [[255u8; 16]; 256];
1407 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
1429pub struct SculptEngine;
1434
1435impl SculptEngine {
1436 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 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 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 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 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 if falloff * params.strength > 0.5 {
1512 to_remove.insert(i);
1513 }
1514 }
1515 }
1516
1517 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1765fn 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 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 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
1809pub 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 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 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 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 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 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#[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 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 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#[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
2187pub struct ModelIO;
2192
2193impl ModelIO {
2194 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 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 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 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 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
2272pub struct ParticlePicker;
2277
2278impl ParticlePicker {
2279 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 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 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 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
2337pub struct VisualizationHelper;
2342
2343impl VisualizationHelper {
2344 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 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 let centroid = dists.iter()
2362 .map(|(j, _)| positions[*j])
2363 .fold(pi, |a, b| a + b)
2364 / (dists.len() + 1) as f32;
2365
2366 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 let oriented = if normal.dot(pi) > 0.0 { normal } else { -normal };
2381 particles[i].normal = oriented;
2382 }
2383 }
2384
2385 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 avg_dist > density_threshold
2405 }).collect()
2406 }
2407
2408 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 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 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 pub fn normal_color_overlay(particles: &mut Vec<ModelParticle>) {
2446 for p in particles.iter_mut() {
2447 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 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 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 results
2496 }
2497}
2498
2499#[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 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, ¶ms, active_char, active_color, &symmetry);
2616 }
2617 BrushKind::Remove => {
2618 SculptEngine::apply_remove(model, hit_pos, ¶ms, &symmetry);
2619 }
2620 BrushKind::Smooth => {
2621 SculptEngine::apply_smooth(model, hit_pos, ¶ms);
2622 }
2623 BrushKind::Inflate => {
2624 SculptEngine::apply_inflate(model, hit_pos, ¶ms);
2625 }
2626 BrushKind::Pinch => {
2627 SculptEngine::apply_pinch(model, hit_pos, ¶ms);
2628 }
2629 BrushKind::Color => {
2630 SculptEngine::apply_color(model, hit_pos, ¶ms);
2631 }
2632 BrushKind::Char => {
2633 SculptEngine::apply_char(model, hit_pos, ¶ms);
2634 }
2635 BrushKind::Flatten => {
2636 SculptEngine::apply_flatten(model, hit_pos, ¶ms);
2637 }
2638 BrushKind::Crease => {
2639 SculptEngine::apply_crease(model, hit_pos, ¶ms);
2640 }
2641 BrushKind::Clone => {
2642 let target = hit_pos + Vec3::new(params.radius * 2.0, 0.0, 0.0);
2644 SculptEngine::apply_clone(model, hit_pos, target, ¶ms);
2645 }
2646 }
2647 model.recompute_bounds();
2648 }
2649 }
2650
2651 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 } 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 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 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 ¤t {
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 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 pub fn generate_lods(&mut self) {
2984 if let Some(model) = self.active_model_mut() {
2985 model.generate_lods();
2986 }
2987 }
2988
2989 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 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 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 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 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 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 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 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 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 }
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
3273pub fn project_onto_plane(v: Vec3, plane_normal: Vec3) -> Vec3 {
3279 let n = plane_normal.normalize();
3280 v - n * n.dot(v)
3281}
3282
3283pub 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
3289pub 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
3296pub 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
3308pub 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
3317pub 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
3333pub 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
3342pub 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
3348pub 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
3354pub fn sdf_sphere(p: Vec3, center: Vec3, radius: f32) -> f32 {
3356 (p - center).length() - radius
3357}
3358
3359pub 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
3365pub 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#[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 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
3418pub struct AdvancedSculpt;
3423
3424impl AdvancedSculpt {
3425 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 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 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 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 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 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 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 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 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 pub fn convex_hull_center(model: &ParticleModel) -> Vec3 {
3616 model.bounds.center()
3617 }
3618
3619 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
3633pub struct ParticleEffects;
3638
3639impl ParticleEffects {
3640 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 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 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 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 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 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
3726pub 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 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
3743pub 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
3751pub 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
3760pub 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
3769pub 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
3781pub 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#[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 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 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
4009pub struct ModelSerializer;
4014
4015impl ModelSerializer {
4016 pub fn serialize_compact(model: &ParticleModel) -> Vec<u8> {
4018 let mut out = Vec::new();
4019 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 out.extend_from_slice(&(model.particles.len() as u32).to_le_bytes());
4027 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 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; 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#[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
4142pub struct GridHelper;
4147
4148impl GridHelper {
4149 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 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 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 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#[derive(Clone, Debug)]
4226pub struct CatmullRomSpline {
4227 pub control_points: Vec<Vec3>,
4228 pub alpha: f32, }
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 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 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 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 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#[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#[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 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
4494pub 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#[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#[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
4794pub 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
4878fn 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#[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#[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#[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#[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#[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
5185pub 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#[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
5328pub 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#[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
5440pub 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
5481pub 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
5517pub 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#[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 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#[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 }, }
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#[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 for p in &mut self.particles {
6048 p.apply_force(self.gravity * p.mass);
6049 }
6050 let springs = self.springs.clone();
6052 for s in &springs {
6053 s.apply(&mut self.particles);
6054 }
6055 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#[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 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 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 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 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 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 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 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 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 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 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#[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 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 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#[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#[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#[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 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 for (i, _from, to) in &self.moved {
6915 if *i < model.particles.len() {
6916 model.particles[*i].position = *to;
6917 }
6918 }
6919 for (i, _from, to) in &self.recolored {
6921 if *i < model.particles.len() {
6922 model.particles[*i].color = *to;
6923 }
6924 }
6925 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#[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 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 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#[derive(Clone, Debug)]
7089pub enum CsgOperation {
7090 Union,
7091 Subtract,
7092 Intersect,
7093 Difference,
7094}
7095
7096pub struct ParticleCsg;
7097
7098impl ParticleCsg {
7099 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 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 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 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 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 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#[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#[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 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 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 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#[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 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#[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, });
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#[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 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 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 let b = make_sphere_model(10); 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 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 let changed = model.particles.iter().filter(|p| p.character == '@').count();
7880 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 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 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
7974pub 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
7982pub 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
7988pub 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
7999pub 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
8007pub fn project_onto_plane_normalized(v: Vec3, normal: Vec3) -> Vec3 {
8009 (v - normal * normal.dot(v)).normalize_or_zero()
8010}
8011
8012pub fn reflect_vector(v: Vec3, normal: Vec3) -> Vec3 {
8014 v - normal * 2.0 * normal.dot(v)
8015}
8016
8017pub fn rotate_around_axis(v: Vec3, axis: Vec3, angle: f32) -> Vec3 {
8019 Quat::from_axis_angle(axis.normalize_or_zero(), angle) * v
8020}
8021
8022pub 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
8049pub 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
8059pub 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
8065pub 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
8079pub 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;