1use crate::field::{Surface, smooth_min};
2use crate::math::{add, length, normalize};
3use crate::mesh::Mesh;
4use crate::shapes::{Shape, round_cone};
5
6pub const VOXEL: f32 = 0.01;
7pub const RELAX: u32 = 8;
8pub const BLEND_VOXELS: f32 = 2.0;
9pub const CROSS_LIMIT: f32 = 900.0;
10pub const ERROR: f32 = 0.001;
11
12#[derive(Clone, Debug, PartialEq)]
13pub enum PartShape {
14 Ball {
15 at: [f32; 2],
16 z: f32,
17 r: f32,
18 },
19 Drop {
20 at: [f32; 2],
21 z: f32,
22 r: f32,
23 squash: f32,
24 stretch: f32,
25 },
26 Capsule {
27 from: [f32; 2],
28 to: [f32; 2],
29 z: f32,
30 r: f32,
31 },
32 Tube {
33 points: Vec<[f32; 2]>,
34 z: f32,
35 r: f32,
36 },
37 Torus {
38 at: [f32; 2],
39 z: f32,
40 major: f32,
41 minor: f32,
42 },
43 Box {
44 at: [f32; 2],
45 z: f32,
46 half: [f32; 3],
47 round: f32,
48 angle: f32,
49 },
50}
51
52impl PartShape {
53 pub fn ball() -> Self {
54 PartShape::Ball {
55 at: [0.0, 0.0],
56 z: 0.0,
57 r: 0.3,
58 }
59 }
60
61 pub fn drop() -> Self {
62 PartShape::Drop {
63 at: [0.0, 0.0],
64 z: 0.0,
65 r: 1.0,
66 squash: 0.45,
67 stretch: 1.0,
68 }
69 }
70
71 pub fn capsule(from: [f32; 2], to: [f32; 2]) -> Self {
72 PartShape::Capsule {
73 from,
74 to,
75 z: 0.0,
76 r: 0.08,
77 }
78 }
79
80 pub fn tube(points: Vec<[f32; 2]>) -> Self {
81 PartShape::Tube {
82 points,
83 z: 0.0,
84 r: 0.08,
85 }
86 }
87
88 pub fn torus() -> Self {
89 PartShape::Torus {
90 at: [0.0, 0.0],
91 z: 0.0,
92 major: 0.6,
93 minor: 0.1,
94 }
95 }
96
97 pub fn cube() -> Self {
98 PartShape::Box {
99 at: [0.0, 0.0],
100 z: 0.0,
101 half: [0.3, 0.3, 0.1],
102 round: 0.05,
103 angle: 0.0,
104 }
105 }
106
107 pub fn objects(&self) -> usize {
108 match self {
109 PartShape::Capsule { .. } | PartShape::Tube { .. } => 3,
110 _ => 1,
111 }
112 }
113
114 pub fn distance(&self, p: [f32; 3]) -> f32 {
115 match self {
116 PartShape::Ball { at, z, r } => length([p[0] - at[0], p[1] - at[1], p[2] - z]) - r,
117 PartShape::Drop {
118 at,
119 z,
120 r,
121 squash,
122 stretch,
123 } => {
124 let radii = [*r, r * stretch, r * squash];
125 let q = [p[0] - at[0], p[1] - at[1], p[2] - z];
126 let k = length([q[0] / radii[0], q[1] / radii[1], q[2] / radii[2]]);
127 (k - 1.0) * radii[0].min(radii[1]).min(radii[2])
128 }
129 PartShape::Capsule { from, to, z, r } => {
130 let a = [from[0], from[1], *z];
131 let b = [to[0], to[1], *z];
132 round_cone(p, a, b, *r, *r)
133 }
134 PartShape::Tube { points, z, r } => {
135 if points.len() == 1 {
136 return length([p[0] - points[0][0], p[1] - points[0][1], p[2] - z]) - r;
137 }
138 points
139 .windows(2)
140 .map(|w| segment_distance(p, [w[0][0], w[0][1], *z], [w[1][0], w[1][1], *z]))
141 .fold(f32::INFINITY, f32::min)
142 - r
143 }
144 PartShape::Torus {
145 at,
146 z,
147 major,
148 minor,
149 } => {
150 let q = [p[0] - at[0], p[1] - at[1], p[2] - z];
151 let ring = (q[0] * q[0] + q[1] * q[1]).sqrt() - major;
152 (ring * ring + q[2] * q[2]).sqrt() - minor
153 }
154 PartShape::Box {
155 at,
156 z,
157 half,
158 round,
159 angle,
160 } => {
161 let (s, c) = angle.to_radians().sin_cos();
162 let d = [p[0] - at[0], p[1] - at[1], p[2] - z];
163 let local = [c * d[0] + s * d[1], -s * d[0] + c * d[1], d[2]];
164 let radius = box_round(*half, *round);
165 let q = [
166 local[0].abs() - half[0] + radius,
167 local[1].abs() - half[1] + radius,
168 local[2].abs() - half[2] + radius,
169 ];
170 length([q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)])
171 + q[0].max(q[1]).max(q[2]).min(0.0)
172 - radius
173 }
174 }
175 }
176
177 pub fn bounds(&self) -> ([f32; 3], [f32; 3]) {
178 let around = |at: [f32; 2], z: f32, e: [f32; 3]| {
179 (
180 [at[0] - e[0], at[1] - e[1], z - e[2]],
181 [at[0] + e[0], at[1] + e[1], z + e[2]],
182 )
183 };
184 match self {
185 PartShape::Ball { at, z, r } => around(*at, *z, [*r; 3]),
186 PartShape::Drop {
187 at,
188 z,
189 r,
190 squash,
191 stretch,
192 } => around(*at, *z, [*r, r * stretch, r * squash]),
193 PartShape::Capsule { from, to, z, r } => line_bounds(&[*from, *to], *z, *r),
194 PartShape::Tube { points, z, r } => line_bounds(points, *z, *r),
195 PartShape::Torus {
196 at,
197 z,
198 major,
199 minor,
200 } => around(*at, *z, [major + minor, major + minor, *minor]),
201 PartShape::Box {
202 at, z, half, angle, ..
203 } => {
204 let (s, c) = angle.to_radians().sin_cos();
205 let ex = c.abs() * half[0] + s.abs() * half[1];
206 let ey = s.abs() * half[0] + c.abs() * half[1];
207 around(*at, *z, [ex, ey, half[2]])
208 }
209 }
210 }
211
212 pub fn mesh(&self, error: f32) -> Mesh {
213 match self {
214 PartShape::Ball { at, z, r } => {
215 let base = Shape::Ellipsoid {
216 radius: *r,
217 squash: 1.0,
218 }
219 .mesh(error);
220 place(base, [at[0], at[1], *z], 0.0, 1.0)
221 }
222 PartShape::Drop {
223 at,
224 z,
225 r,
226 squash,
227 stretch,
228 } => {
229 let base = Shape::Ellipsoid {
230 radius: *r,
231 squash: *squash,
232 }
233 .mesh(error);
234 place(base, [at[0], at[1], *z], 0.0, *stretch)
235 }
236 PartShape::Capsule { from, to, z, r } => Shape::Capsule {
237 a: [from[0], from[1], *z],
238 b: [to[0], to[1], *z],
239 radius: *r,
240 }
241 .mesh(error),
242 PartShape::Tube { points, z, r } => {
243 let pieces = if points.len() == 1 {
244 vec![
245 PartShape::Ball {
246 at: points[0],
247 z: *z,
248 r: *r,
249 }
250 .mesh(error),
251 ]
252 } else {
253 points
254 .windows(2)
255 .map(|w| {
256 Shape::Capsule {
257 a: [w[0][0], w[0][1], *z],
258 b: [w[1][0], w[1][1], *z],
259 radius: *r,
260 }
261 .mesh(error)
262 })
263 .collect()
264 };
265 join(&pieces)
266 }
267 PartShape::Torus {
268 at,
269 z,
270 major,
271 minor,
272 } => place(
273 Shape::Torus {
274 major: *major,
275 minor: *minor,
276 }
277 .mesh(error),
278 [at[0], at[1], *z],
279 0.0,
280 1.0,
281 ),
282 PartShape::Box {
283 at,
284 z,
285 half,
286 round,
287 angle,
288 } => place(
289 Shape::RoundBox {
290 half: *half,
291 radius: box_round(*half, *round),
292 }
293 .mesh(error),
294 [at[0], at[1], *z],
295 *angle,
296 1.0,
297 ),
298 }
299 }
300}
301
302fn box_round(half: [f32; 3], round: f32) -> f32 {
303 round.min(half[0].min(half[1]).min(half[2]) * 0.9).max(0.0)
304}
305
306fn line_bounds(points: &[[f32; 2]], z: f32, r: f32) -> ([f32; 3], [f32; 3]) {
307 let mut lo = [f32::INFINITY, f32::INFINITY, z - r];
308 let mut hi = [f32::NEG_INFINITY, f32::NEG_INFINITY, z + r];
309 for p in points {
310 for i in 0..2 {
311 lo[i] = lo[i].min(p[i] - r);
312 hi[i] = hi[i].max(p[i] + r);
313 }
314 }
315 (lo, hi)
316}
317
318fn segment_distance(p: [f32; 3], a: [f32; 3], b: [f32; 3]) -> f32 {
319 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
320 let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
321 let l2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
322 let t = if l2 <= 1e-12 {
323 0.0
324 } else {
325 ((ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / l2).clamp(0.0, 1.0)
326 };
327 length([ap[0] - ab[0] * t, ap[1] - ab[1] * t, ap[2] - ab[2] * t])
328}
329
330fn place(mesh: Mesh, at: [f32; 3], angle: f32, stretch: f32) -> Mesh {
331 let (s, c) = angle.to_radians().sin_cos();
332 let positions = mesh
333 .positions
334 .iter()
335 .map(|p| {
336 let y = p[1] * stretch;
337 add([c * p[0] - s * y, s * p[0] + c * y, p[2]], at)
338 })
339 .collect();
340 let normals = mesh
341 .normals
342 .iter()
343 .map(|n| {
344 let y = n[1] / stretch;
345 normalize([c * n[0] - s * y, s * n[0] + c * y, n[2]])
346 })
347 .collect();
348 Mesh::new(positions, normals, Vec::new(), mesh.uvs, mesh.indices)
349}
350
351pub(crate) fn join(meshes: &[Mesh]) -> Mesh {
352 let mut positions = Vec::new();
353 let mut normals = Vec::new();
354 let mut tangents = Vec::new();
355 let mut uvs = Vec::new();
356 let mut indices = Vec::new();
357 for mesh in meshes {
358 let base = positions.len() as u32;
359 positions.extend_from_slice(&mesh.positions);
360 normals.extend_from_slice(&mesh.normals);
361 tangents.extend_from_slice(&mesh.tangents);
362 uvs.extend_from_slice(&mesh.uvs);
363 indices.extend(mesh.indices.iter().map(|i| i + base));
364 }
365 Mesh::new(positions, normals, tangents, uvs, indices)
366}
367
368#[derive(Clone, Debug, PartialEq)]
369pub struct Part {
370 pub shape: PartShape,
371 pub material: Option<String>,
372}
373
374impl Part {
375 pub fn new(shape: PartShape) -> Self {
376 Self {
377 shape,
378 material: None,
379 }
380 }
381
382 pub fn with_material(mut self, material: &str) -> Self {
383 self.material = Some(material.to_string());
384 self
385 }
386}
387
388#[derive(Clone, Debug, PartialEq)]
389pub struct Icon {
390 pub parts: Vec<Part>,
391 pub fuse: bool,
392 pub voxel: f32,
393 pub blend: Option<f32>,
394 pub relax: u32,
395 pub error: f32,
396}
397
398impl Icon {
399 pub fn new(parts: Vec<Part>) -> Self {
400 Self {
401 parts,
402 fuse: true,
403 voxel: VOXEL,
404 blend: None,
405 relax: RELAX,
406 error: ERROR,
407 }
408 }
409
410 pub fn build(&self) -> Result<Vec<Piece>, String> {
411 for (i, part) in self.parts.iter().enumerate() {
412 check(i, &part.shape)?;
413 }
414 if !(self.voxel > 0.0 && self.voxel.is_finite()) {
415 return Err(format!("icon: voxel must be above 0, got {}", self.voxel));
416 }
417 let mut groups: Vec<(Option<String>, Vec<usize>)> = Vec::new();
418 for (i, part) in self.parts.iter().enumerate() {
419 match groups.iter_mut().find(|(name, _)| *name == part.material) {
420 Some((_, members)) => members.push(i),
421 None => groups.push((part.material.clone(), vec![i])),
422 }
423 }
424 let mut pieces = Vec::new();
425 for (material, members) in groups {
426 let objects: usize = members.iter().map(|&i| self.parts[i].shape.objects()).sum();
427 if self.fuse && objects > 1 {
428 let shapes = members
429 .iter()
430 .map(|&i| &self.parts[i].shape)
431 .collect::<Vec<_>>();
432 pieces.push(Piece {
433 material,
434 parts: members,
435 mesh: self.fused(&shapes),
436 });
437 } else {
438 for i in members {
439 pieces.push(Piece {
440 material: material.clone(),
441 parts: vec![i],
442 mesh: self.parts[i].shape.mesh(self.error),
443 });
444 }
445 }
446 }
447 Ok(pieces)
448 }
449
450 pub fn voxel_for(&self, shapes: &[&PartShape]) -> f32 {
451 let (lo, hi) = union_bounds(shapes);
452 let span = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]);
453 self.voxel.max(span / CROSS_LIMIT)
454 }
455
456 fn fused(&self, shapes: &[&PartShape]) -> Mesh {
457 let voxel = self.voxel_for(shapes);
458 let blend = self.blend.unwrap_or(BLEND_VOXELS * voxel).max(0.0);
459 let (lo, hi) = union_bounds(shapes);
460 let field = |p: [f32; 3]| {
461 shapes
462 .iter()
463 .map(|s| s.distance(p))
464 .reduce(|a, b| smooth_min(a, b, blend))
465 .unwrap_or(f32::INFINITY)
466 };
467 Surface::new(voxel, self.relax).mesh(&field, sub3(lo, blend), add3(hi, blend))
468 }
469}
470
471fn add3(p: [f32; 3], d: f32) -> [f32; 3] {
472 [p[0] + d, p[1] + d, p[2] + d]
473}
474
475fn sub3(p: [f32; 3], d: f32) -> [f32; 3] {
476 [p[0] - d, p[1] - d, p[2] - d]
477}
478
479fn union_bounds(shapes: &[&PartShape]) -> ([f32; 3], [f32; 3]) {
480 let mut lo = [f32::INFINITY; 3];
481 let mut hi = [f32::NEG_INFINITY; 3];
482 for shape in shapes {
483 let (a, b) = shape.bounds();
484 for i in 0..3 {
485 lo[i] = lo[i].min(a[i]);
486 hi[i] = hi[i].max(b[i]);
487 }
488 }
489 (lo, hi)
490}
491
492fn check(index: usize, shape: &PartShape) -> Result<(), String> {
493 let positive = |name: &str, v: f32| {
494 if v > 0.0 && v.is_finite() {
495 Ok(())
496 } else {
497 Err(format!(
498 "icon part {index}: {name} must be above 0, got {v}"
499 ))
500 }
501 };
502 match shape {
503 PartShape::Ball { r, .. } => positive("r", *r),
504 PartShape::Drop {
505 r, squash, stretch, ..
506 } => {
507 positive("r", *r)?;
508 positive("squash", *squash)?;
509 positive("stretch", *stretch)
510 }
511 PartShape::Capsule { r, .. } => positive("r", *r),
512 PartShape::Tube { points, r, .. } => {
513 if points.is_empty() {
514 return Err(format!("icon part {index}: a tube needs points"));
515 }
516 positive("r", *r)
517 }
518 PartShape::Torus { major, minor, .. } => {
519 positive("major", *major)?;
520 positive("minor", *minor)
521 }
522 PartShape::Box { half, .. } => {
523 for v in half {
524 positive("half", *v)?;
525 }
526 Ok(())
527 }
528 }
529}
530
531#[derive(Clone, Debug)]
532pub struct Piece {
533 pub material: Option<String>,
534 pub parts: Vec<usize>,
535 pub mesh: Mesh,
536}