1use super::{Cutter, CutterProfile, SegmentKind};
14use crate::model::{Bounds3, Holder, Point3};
15use serde::{Deserialize, Serialize};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "kebab-case")]
20pub enum ToolPart {
21 Cutting,
22 Shank,
23 Holder,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct ProfilePoint {
30 pub radius: f64,
31 pub height: f64,
32 pub part: ToolPart,
33}
34
35#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ToolProfile {
39 pub points: Vec<ProfilePoint>,
42}
43
44pub const DEFAULT_CHORD: f64 = 0.01;
46pub const DEFAULT_RADIAL_SEGMENTS: usize = 32;
48
49impl ToolProfile {
50 pub fn new(cutter: &Cutter, holder: Option<&Holder>, chord: f64) -> Self {
53 let chord = if chord.is_finite() && chord > 0.0 { chord } else { DEFAULT_CHORD };
54 let mut points: Vec<ProfilePoint> = Vec::new();
55 let mut push = |radius: f64, height: f64, part: ToolPart| {
58 let p = ProfilePoint { radius: radius.max(0.0), height, part };
59 match points.last_mut() {
60 Some(last) if last.radius == p.radius && last.height == p.height => last.part = part,
61 _ => points.push(p),
62 }
63 };
64 for seg in cutter.segments() {
65 let [r0, r1] = seg.radius_range;
66 let [h0, h1] = seg.height_range;
67 match seg.kind {
68 SegmentKind::Flat | SegmentKind::Cone | SegmentKind::Cylinder | SegmentKind::ConeInverted => {
69 push(r0, h0, ToolPart::Cutting);
70 push(r1, h1, ToolPart::Cutting);
71 }
72 SegmentKind::Sphere | SegmentKind::Torus | SegmentKind::SphereUpper => {
73 let rho = seg.profile_radius;
77 let center_r = if seg.kind == SegmentKind::Torus { seg.ring_radius } else { 0.0 };
78 let upper = seg.kind == SegmentKind::SphereUpper;
79 let angle_at = |r: f64| {
80 let a = ((r - center_r) / rho).clamp(-1.0, 1.0).asin();
81 if upper { std::f64::consts::PI - a } else { a }
82 };
83 let (a0, a1) = (angle_at(r0), angle_at(r1));
84 let steps = arc_steps(rho, (a1 - a0).abs(), chord);
85 push(r0, h0, ToolPart::Cutting);
86 for i in 1..steps {
87 let a = a0 + (a1 - a0) * i as f64 / steps as f64;
88 push(center_r + rho * a.sin(), seg.z_offset - rho * a.cos(), ToolPart::Cutting);
89 }
90 push(r1, h1, ToolPart::Cutting);
91 }
92 }
93 }
94 let cutting_top = cutter.cutting_length();
95 let shank_r = cutter.shaft_radius();
96 let shank_top = cutting_top + cutter.shaft_length();
97 if cutter.shaft_length() > 0.0 {
98 push(shank_r, cutting_top, ToolPart::Shank);
99 push(shank_r, shank_top, ToolPart::Shank);
100 }
101 if let Some(holder) = holder {
102 let mut base = shank_top;
103 for seg in &holder.segments {
104 push(0.5 * seg.lower_diameter, base, ToolPart::Holder);
105 base += seg.height;
106 push(0.5 * seg.upper_diameter, base, ToolPart::Holder);
107 }
108 }
109 Self { points }
110 }
111
112 pub fn top_height(&self) -> f64 {
113 self.points.last().map_or(0.0, |p| p.height)
114 }
115
116 pub fn max_radius(&self) -> f64 {
117 self.points.iter().map(|p| p.radius).fold(0.0, f64::max)
118 }
119
120 pub fn radius_at_height(&self, h: f64) -> Option<f64> {
123 if !h.is_finite() || h < 0.0 || h > self.top_height() || self.points.is_empty() {
124 return None;
125 }
126 let mut best: Option<f64> = None;
127 for w in self.points.windows(2) {
128 let (a, b) = (w[0], w[1]);
129 if h < a.height.min(b.height) || h > a.height.max(b.height) {
130 continue;
131 }
132 let r = if (b.height - a.height).abs() <= f64::EPSILON * (1.0 + h.abs()) {
133 a.radius.max(b.radius)
134 } else {
135 a.radius + (b.radius - a.radius) * (h - a.height) / (b.height - a.height)
136 };
137 best = Some(best.map_or(r, |x: f64| x.max(r)));
138 }
139 best
140 }
141
142 pub fn height_bands_reaching(&self, r: f64) -> Vec<([f64; 2], ToolPart)> {
147 let mut out: Vec<([f64; 2], ToolPart)> = Vec::new();
148 let mut push = |band: [f64; 2], part: ToolPart| {
149 if band[1] < band[0] {
150 return;
151 }
152 match out.last_mut() {
153 Some((last, lp)) if *lp == part && last[1] >= band[0] => last[1] = last[1].max(band[1]),
154 _ => out.push((band, part)),
155 }
156 };
157 for w in self.points.windows(2) {
158 let (a, b) = (w[0], w[1]);
159 if b.height < a.height {
160 continue;
161 }
162 let (ra, rb) = (a.radius, b.radius);
163 if ra < r && rb < r {
164 continue;
165 }
166 if b.height == a.height {
167 if ra.max(rb) >= r {
169 push([a.height, a.height], a.part);
170 }
171 continue;
172 }
173 let (mut h0, mut h1) = (a.height, b.height);
175 if (rb - ra).abs() > 0.0 {
176 let h_cross = a.height + (r - ra) * (b.height - a.height) / (rb - ra);
177 if ra < r {
178 h0 = h_cross.max(a.height);
179 } else if rb < r {
180 h1 = h_cross.min(b.height);
181 }
182 }
183 push([h0, h1], a.part);
184 }
185 out
186 }
187
188 pub fn bounds(&self) -> Bounds3 {
190 let r = self.max_radius();
191 Bounds3::new(Point3::new(-r, -r, 0.0), Point3::new(r, r, self.top_height()))
192 }
193
194 pub fn mesh(&self, radial_segments: usize) -> ToolMesh {
197 self.swept_mesh(Point3::ZERO, Point3::ZERO, radial_segments)
198 }
199
200 fn sided_radii(&self, h: f64) -> Option<(f64, f64)> {
204 let mut below: Option<f64> = None;
205 let mut above: Option<f64> = None;
206 let bump = |slot: &mut Option<f64>, r: f64| *slot = Some(slot.map_or(r, |x: f64| x.max(r)));
207 for w in self.points.windows(2) {
208 let (a, b) = (w[0], w[1]);
209 let (lo, hi) = (a.height.min(b.height), a.height.max(b.height));
210 if h < lo || h > hi {
211 continue;
212 }
213 if hi > lo {
214 let r = a.radius + (b.radius - a.radius) * (h - a.height) / (b.height - a.height);
215 if h > lo {
216 bump(&mut below, r);
217 }
218 if h < hi {
219 bump(&mut above, r);
220 }
221 if h == lo {
222 bump(&mut above, r);
223 }
224 if h == hi {
225 bump(&mut below, r);
226 }
227 } else {
228 bump(&mut below, a.radius);
231 bump(&mut above, b.radius);
232 }
233 }
234 if let Some(first) = self.points.first() {
235 if h == first.height {
236 bump(&mut above, first.radius);
237 bump(&mut below, first.radius);
238 }
239 }
240 if let Some(last) = self.points.last() {
241 if h == last.height {
242 bump(&mut below, last.radius);
243 bump(&mut above, last.radius);
244 }
245 }
246 match (below, above) {
247 (None, None) => None,
248 (b, a) => Some((b.or(a).unwrap(), a.or(b).unwrap())),
249 }
250 }
251
252 pub fn part_at(&self, h: f64) -> ToolPart {
255 self.points.iter().rev().find(|p| p.height <= h).or(self.points.first()).map_or(ToolPart::Cutting, |p| p.part)
256 }
257
258 pub fn vertical_envelope(&self, dz: f64) -> ToolProfile {
262 if !(dz > 0.0) || !dz.is_finite() || self.points.is_empty() {
263 return self.clone();
264 }
265 let mut heights: Vec<f64> = self.points.iter().flat_map(|p| [p.height, p.height + dz]).collect();
266 heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
267 heights.dedup();
268 let mut out: Vec<ProfilePoint> = Vec::new();
269 let mut push = |radius: f64, height: f64, part: ToolPart| {
270 let p = ProfilePoint { radius, height, part };
271 match out.last_mut() {
272 Some(last) if last.radius == p.radius && last.height == p.height => last.part = part,
273 _ => out.push(p),
274 }
275 };
276 for h in heights {
277 let orig = self.sided_radii(h);
278 let lifted = self.sided_radii(h - dz);
279 let pick = |o: Option<f64>, l: Option<f64>| match (o, l) {
280 (Some(a), Some(b)) => Some(a.max(b)),
281 (a, b) => a.or(b),
282 };
283 let below = pick(orig.map(|r| r.0), lifted.map(|r| r.0));
284 let above = pick(orig.map(|r| r.1), lifted.map(|r| r.1));
285 let part = if orig.is_some() { self.part_at(h) } else { self.part_at(h - dz) };
286 if let Some(b) = below {
287 push(b, h, part);
288 }
289 if let Some(a) = above {
290 push(a, h, part);
291 }
292 }
293 ToolProfile { points: out }
294 }
295
296 pub fn swept_mesh(&self, start: Point3, end: Point3, radial_segments: usize) -> ToolMesh {
309 let n = radial_segments.max(3);
310 let d = end.sub(start);
311 let dz = d.z.abs();
312 let envelope = self.vertical_envelope(dz);
313 let z0 = start.z.min(end.z);
314 let a = Point3::new(start.x, start.y, z0);
315 let b = Point3::new(end.x, end.y, z0);
316 let dxy = Point3::new(d.x, d.y, 0.0);
317 let dir = dxy.normalized().unwrap_or(Point3::X);
319 let moving = dxy.length() > 0.0;
320 let half = (n / 2).max(1);
321 let ring_len = if moving { 2 * (half + 1) } else { n };
322 let mut mesh = ToolMesh::default();
323 let mut rings: Vec<(u32, &ProfilePoint)> = Vec::with_capacity(envelope.points.len());
324 for p in &envelope.points {
325 let base = mesh.positions.len() as u32;
326 mesh.push_ring(a, b, dir, moving, p.radius, p.height, n, half);
327 rings.push((base, p));
328 }
329 let Some((first_base, first)) = rings.first().copied() else {
330 return mesh;
331 };
332 if first.radius > 0.0 {
335 mesh.cap(first_base, ring_len, first.part, true);
336 }
337 for w in rings.windows(2) {
339 let ((ra, pa), (rb, _)) = (w[0], w[1]);
340 mesh.band(ra, rb, ring_len, pa.part);
341 }
342 if let Some((last_base, last)) = rings.last().copied() {
343 if last.radius > 0.0 {
344 mesh.cap(last_base, ring_len, last.part, false);
345 }
346 }
347 mesh.finish_groups();
348 mesh
349 }
350}
351
352fn arc_steps(rho: f64, sweep: f64, chord: f64) -> usize {
354 if !(rho > 0.0) || !(sweep > 0.0) {
355 return 1;
356 }
357 let max_step = 2.0 * (1.0 - (chord / rho).min(1.0)).acos();
358 if !(max_step > 0.0) {
359 return 1;
360 }
361 ((sweep / max_step).ceil() as usize).clamp(1, 4096)
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct ToolMeshGroup {
368 pub part: ToolPart,
369 pub start: u32,
371 pub end: u32,
372}
373
374#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
376#[serde(rename_all = "camelCase")]
377pub struct ToolMesh {
378 pub positions: Vec<Point3>,
379 pub indices: Vec<u32>,
381 pub groups: Vec<ToolMeshGroup>,
382 #[serde(skip)]
383 pending: Vec<(ToolPart, u32)>,
384}
385
386impl ToolMesh {
387 pub fn triangle_count(&self) -> usize {
388 crate::mesh::core::MeshCore { positions: &self.positions, indices: &self.indices }.triangle_count()
389 }
390
391 pub fn triangle(&self, i: usize) -> [Point3; 3] {
392 crate::mesh::core::MeshCore { positions: &self.positions, indices: &self.indices }.triangle(i)
393 }
394
395 pub fn signed_volume(&self) -> f64 {
397 crate::mesh::core::MeshCore { positions: &self.positions, indices: &self.indices }.signed_volume()
398 }
399
400 pub fn bounds(&self) -> Bounds3 {
401 crate::mesh::core::MeshCore { positions: &self.positions, indices: &self.indices }.bounds()
402 }
403
404 pub fn is_closed(&self) -> bool {
408 crate::mesh::core::MeshCore { positions: &self.positions, indices: &self.indices }.is_closed()
409 }
410
411 fn push_ring(&mut self, start: Point3, end: Point3, dir: Point3, moving: bool, radius: f64, height: f64, n: usize, half: usize) {
416 use std::f64::consts::PI;
417 let side = Point3::new(-dir.y, dir.x, 0.0);
418 let at = |c: Point3, ang: f64| {
419 c.add(dir.scale(radius * ang.cos())).add(side.scale(radius * ang.sin())).add(Point3::new(0.0, 0.0, height))
420 };
421 if !moving {
422 for i in 0..n {
423 self.positions.push(at(start, 2.0 * PI * i as f64 / n as f64));
424 }
425 return;
426 }
427 for i in 0..=half {
429 self.positions.push(at(start, 0.5 * PI + PI * i as f64 / half as f64));
430 }
431 for i in 0..=half {
433 self.positions.push(at(end, 1.5 * PI + PI * i as f64 / half as f64));
434 }
435 }
436
437 fn push_triangle(&mut self, part: ToolPart, a: u32, b: u32, c: u32) {
438 if a == b || b == c || a == c {
439 return;
440 }
441 let [pa, pb, pc] = [self.positions[a as usize], self.positions[b as usize], self.positions[c as usize]];
442 if pb.sub(pa).cross(pc.sub(pa)).length_squared() <= 0.0 {
443 return;
444 }
445 self.indices.extend([a, b, c]);
446 self.pending.push((part, (self.indices.len() / 3 - 1) as u32));
447 }
448
449 fn band(&mut self, a: u32, b: u32, len: usize, part: ToolPart) {
451 let len = len as u32;
452 for i in 0..len {
453 let j = (i + 1) % len;
454 self.push_triangle(part, a + i, a + j, b + j);
455 self.push_triangle(part, a + i, b + j, b + i);
456 }
457 }
458
459 fn cap(&mut self, base: u32, len: usize, part: ToolPart, down: bool) {
461 let center = self.positions.len() as u32;
462 let mut c = Point3::ZERO;
463 for i in 0..len {
464 c = c.add(self.positions[(base + i as u32) as usize]);
465 }
466 self.positions.push(c.scale(1.0 / len as f64));
467 let len = len as u32;
468 for i in 0..len {
469 let j = (i + 1) % len;
470 if down {
471 self.push_triangle(part, center, base + j, base + i);
472 } else {
473 self.push_triangle(part, center, base + i, base + j);
474 }
475 }
476 }
477
478 fn finish_groups(&mut self) {
480 let mut groups: Vec<ToolMeshGroup> = Vec::new();
481 for &(part, index) in &self.pending {
482 match groups.last_mut() {
483 Some(g) if g.part == part && g.end == index => g.end = index + 1,
484 _ => groups.push(ToolMeshGroup { part, start: index, end: index + 1 }),
485 }
486 }
487 self.groups = groups;
488 self.pending.clear();
489 }
490
491 pub fn part_of(&self, i: u32) -> Option<ToolPart> {
493 self.groups.iter().find(|g| g.start <= i && i < g.end).map(|g| g.part)
494 }
495}