1use super::*;
2
3pub(super) struct RimPiece {
5 pub(super) face_id: u64,
6 pub(super) loop_index: usize,
7 pub(super) window: [f64; 2],
9 pub(super) edge_id: u64,
10}
11
12pub fn blend_smooth_chain(
18 solid: &BrepSolid,
19 seed_edge_id: u64,
20 radius: f64,
21 chamfer: bool,
22 name: Option<&str>,
23) -> Result<BrepSolid, String> {
24 if !(radius > 0.0) || !radius.is_finite() {
25 return Err("blend: radius must be positive".into());
26 }
27 let chain = collect_smooth_chain(solid, seed_edge_id)?;
28 if chain.segments.len() < 2 {
29 return Err("blend: chain collapsed to a single segment".into());
30 }
31 if chain.closed {
32 blend_closed_smooth_chain(solid, &chain.segments, radius, chamfer, name)
33 } else {
34 blend_open_smooth_chain(solid, &chain, radius, chamfer, name)
35 }
36}
37
38pub fn blend_smooth_chain_if_closed(
45 solid: &BrepSolid,
46 seed_edge_id: u64,
47 radius: f64,
48 chamfer: bool,
49 name: Option<&str>,
50) -> Result<BrepSolid, String> {
51 if !(radius > 0.0) || !radius.is_finite() {
52 return Err("blend: radius must be positive".into());
53 }
54 let chain = collect_smooth_chain(solid, seed_edge_id)?;
55 if chain.segments.len() < 2 {
56 return Err("blend: chain collapsed to a single segment".into());
57 }
58 if !chain.closed {
59 return Err(
60 "blend: the edge is one arc of an OPEN tangent chain; the unselected \
61 continuation is capped, not blended"
62 .into(),
63 );
64 }
65 blend_closed_smooth_chain(solid, &chain.segments, radius, chamfer, name)
66}
67
68fn blend_closed_smooth_chain(
71 solid: &BrepSolid,
72 segments: &[ChainSegment<'_>],
73 radius: f64,
74 chamfer: bool,
75 name: Option<&str>,
76) -> Result<BrepSolid, String> {
77 let samples = march_chain(segments, radius, false)?;
78
79 let mut global: Vec<(f64, &ChainSample)> = samples
81 .iter()
82 .filter(|sample| (0..CHAIN_PER_SEGMENT as isize).contains(&sample.position))
83 .map(|sample| (sample.parameter.rem_euclid(1.0), sample))
84 .collect();
85 global.sort_by(|a, b| a.0.total_cmp(&b.0));
86 let count = global.len();
87 let degree = FIT_DEGREE;
88 let mut extended_params = Vec::new();
89 let mut samples_cr = Vec::new();
90 let mut samples_mid = Vec::new();
91 let mut samples_cs = Vec::new();
92 let mut push = |station: &Station, parameter: f64| {
93 extended_params.push(parameter);
94 samples_cr.push(Vec4::from_point(station.p1, 1.0));
95 samples_cs.push(Vec4::from_point(station.p2, 1.0));
96 samples_mid.push(Vec4 {
97 x: station.apex.x * station.weight,
98 y: station.apex.y * station.weight,
99 z: station.apex.z * station.weight,
100 w: station.weight,
101 });
102 };
103 for offset in (1..=degree).rev() {
104 let (parameter, sample) = &global[count - offset];
105 push(&sample.station, parameter - 1.0);
106 }
107 for (parameter, sample) in &global {
108 push(&sample.station, *parameter);
109 }
110 push(&global[0].1.station, global[0].0 + 1.0);
113 for offset in 1..=degree {
114 let (parameter, sample) = &global[offset];
115 push(&sample.station, parameter + 1.0);
116 }
117 let low = extended_params[0];
118 let high = *extended_params.last().unwrap();
119 let range = high - low;
120 let normalized: Vec<f64> = extended_params
121 .iter()
122 .map(|parameter| (parameter - low) / range)
123 .collect();
124 let seam_low = (0.0 - low) / range;
125 let seam_high = (1.0 - low) / range;
126 let fit_row = |row: &[Vec4]| -> Result<NurbsCurve, String> {
127 let curve = fit::interpolate_homogeneous(row, degree, &normalized)?;
128 let (_, tail) = curve.split(seam_low)?;
129 let (middle, _) = tail.split(seam_high)?;
130 Ok(middle)
131 };
132 let cr = fit_row(&samples_cr)?;
133 let cs = fit_row(&samples_cs)?;
134 let mid = if chamfer {
135 None
136 } else {
137 Some(fit_row(&samples_mid)?)
138 };
139 let u_domain = cr.domain()?;
140 let rows_u = cr.control_points.len();
141 let mut control = Vec::with_capacity(rows_u);
142 for index in 0..rows_u {
143 let mut column = vec![cr.control_points[index]];
144 if let Some(mid) = &mid {
145 column.push(mid.control_points[index]);
146 }
147 column.push(cs.control_points[index]);
148 control.push(column);
149 }
150 let last = rows_u - 1;
151 for column_index in 0..control[0].len() {
152 control[last][column_index] = control[0][column_index];
153 }
154 let (degree_v, knots_v) = if chamfer {
155 (1usize, vec![0.0, 0.0, 1.0, 1.0])
156 } else {
157 (2usize, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0])
158 };
159 let surface = NurbsSurface::new(degree, degree_v, cr.knots.clone(), knots_v, control)?;
160
161 let single_face = |side: usize| -> bool {
165 let first_id = if side == 0 {
166 segments[0].first.face.id
167 } else {
168 segments[0].second.face.id
169 };
170 segments.iter().all(|segment| {
171 let id = if side == 0 {
172 segment.first.face.id
173 } else {
174 segment.second.face.id
175 };
176 id == first_id
177 })
178 };
179 let mut whole_pcurves: [Option<NurbsCurve>; 2] = [None, None];
180 for side in 0..2 {
181 if !single_face(side) {
182 continue;
183 }
184 let mut row = Vec::new();
185 let select = |station: &Station| -> [f64; 2] {
186 if side == 0 {
187 station.uv1
188 } else {
189 station.uv2
190 }
191 };
192 let mut push_uv = |station: &Station| {
193 let uv = select(station);
194 row.push(Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0));
195 };
196 for offset in (1..=degree).rev() {
197 push_uv(&global[count - offset].1.station);
198 }
199 for (_, sample) in &global {
200 push_uv(&sample.station);
201 }
202 push_uv(&global[0].1.station);
203 for offset in 1..=degree {
204 push_uv(&global[offset].1.station);
205 }
206 whole_pcurves[side] = Some(fit_row(&row)?);
207 }
208
209 let mut pcurves1 = Vec::with_capacity(segments.len());
212 let mut pcurves2 = Vec::with_capacity(segments.len());
213 for segment in 0..segments.len() {
214 let mut window: Vec<&ChainSample> = samples
215 .iter()
216 .filter(|sample| sample.segment == segment)
217 .collect();
218 window.sort_by(|a, b| a.parameter.total_cmp(&b.parameter));
219 let params: Vec<f64> = window.iter().map(|sample| sample.parameter).collect();
220 let low = params[0];
221 let high = *params.last().unwrap();
222 let normalized: Vec<f64> = params
223 .iter()
224 .map(|parameter| (parameter - low) / (high - low))
225 .collect();
226 let fit_uv = |select: &dyn Fn(&Station) -> [f64; 2]| -> Result<NurbsCurve, String> {
227 let points: Vec<Vec4> = window
228 .iter()
229 .map(|sample| {
230 let uv = select(&sample.station);
231 Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0)
232 })
233 .collect();
234 let curve = fit::interpolate_homogeneous(&points, degree, &normalized)?;
235 Ok(curve)
239 };
240 pcurves1.push((low, high, fit_uv(&|station| station.uv1)?));
241 pcurves2.push((low, high, fit_uv(&|station| station.uv2)?));
242 }
243
244 chain_surgery(
245 solid,
246 segments,
247 ChainRows {
248 surface,
249 cr,
250 cs,
251 u_domain,
252 fit_low: low,
253 fit_range: range,
254 pcurves1,
255 pcurves2,
256 whole_pcurves,
257 },
258 name,
259 )
260}
261
262pub(super) struct ChainRows {
263 pub(super) surface: NurbsSurface,
264 pub(super) cr: NurbsCurve,
265 pub(super) cs: NurbsCurve,
266 pub(super) u_domain: [f64; 2],
267 pub(super) fit_low: f64,
271 pub(super) fit_range: f64,
272 pub(super) pcurves1: Vec<(f64, f64, NurbsCurve)>,
275 pub(super) pcurves2: Vec<(f64, f64, NurbsCurve)>,
276 pub(super) whole_pcurves: [Option<NurbsCurve>; 2],
278}
279
280impl ChainRows {
281 pub(super) fn to_global(&self, fit_parameter: f64) -> f64 {
282 self.fit_low + fit_parameter * self.fit_range
283 }
284}
285
286pub(super) fn pcurve_portion(fitted: &(f64, f64, NurbsCurve), a: f64, b: f64) -> Result<NurbsCurve, String> {
290 let (low, high, curve) = fitted;
291 let to_local = |value: f64| {
294 let mut best = value;
295 for candidate in [value, value + 1.0, value - 1.0] {
296 if candidate >= low - 1e-9 && candidate <= high + 1e-9 {
297 best = candidate;
298 break;
299 }
300 }
301 ((best - low) / (high - low)).clamp(0.0, 1.0)
302 };
303 let mut local_a = to_local(a);
304 let mut local_b = to_local(b);
305 if local_a > local_b {
306 std::mem::swap(&mut local_a, &mut local_b);
307 }
308 let epsilon = 1e-9;
309 let (_, tail) = if local_a > epsilon {
310 curve.split(local_a)?
311 } else {
312 (curve.clone(), curve.clone())
313 };
314 let tail = if local_a > epsilon {
315 tail
316 } else {
317 curve.clone()
318 };
319 let portion = if local_b < 1.0 - epsilon {
320 tail.split(local_b)?.0
321 } else {
322 tail
323 };
324 Ok(portion)
325}
326
327pub(super) fn cross_edge_at(
333 solid: &BrepSolid,
334 face_before: &FaceRecord,
335 loop_before: usize,
336 face_after: &FaceRecord,
337 loop_after: usize,
338 vertex: u64,
339 before_edge: u64,
340 after_edge: u64,
341) -> Result<Option<u64>, String> {
342 let mut found: Option<u64> = None;
343 let mut consider = |face: &FaceRecord, loop_index: usize| -> Result<(), String> {
344 for coedge in &face.loops[loop_index].coedges {
345 if coedge.edge_id == before_edge || coedge.edge_id == after_edge {
346 continue;
347 }
348 let edge = solid
349 .edges
350 .iter()
351 .find(|edge| edge.id == coedge.edge_id)
352 .ok_or("blend: loop references missing edge")?;
353 if edge.start_vertex_id == vertex || edge.end_vertex_id == vertex {
354 if found.is_some() && found != Some(edge.id) {
355 return Err("blend: multiple cross edges at a chain vertex".into());
356 }
357 found = Some(edge.id);
358 }
359 }
360 Ok(())
361 };
362 consider(face_before, loop_before)?;
363 if !(face_after.id == face_before.id && loop_after == loop_before) {
364 consider(face_after, loop_after)?;
365 }
366 Ok(found)
367}
368
369fn nearest_seam_image(value: f64, low: f64, high: f64) -> f64 {
372 let period = high - low;
373 low + ((value - low) / period).round() * period
374}
375
376pub(super) fn project_piece_pcurve(
394 piece: &NurbsCurve,
395 surface: &NurbsSurface,
396 start_is_crossing: bool,
397 end_is_crossing: bool,
398) -> Result<NurbsCurve, String> {
399 let [u0, u1] = surface.domain_u()?;
400 let [v0, v1] = surface.domain_v()?;
401 let (_, closed_v) = surface.closed_directions()?;
402 let period = u1 - u0;
403 let period_v = v1 - v0;
404 let [d0, d1] = piece.domain()?;
405 const DENSE: usize = 96;
408 let mut proj_u = Vec::with_capacity(DENSE + 1);
409 let mut proj_v = Vec::with_capacity(DENSE + 1);
410 let mut point3 = Vec::with_capacity(DENSE + 1);
411 let mut params = Vec::with_capacity(DENSE + 1);
412 let mut previous_u: Option<f64> = None;
413 for section in 0..=DENSE {
414 let t = d0 + (d1 - d0) * section as f64 / DENSE as f64;
415 let point = piece.evaluate(t)?;
416 let projection = crate::project_point_to_surface(surface, point)?;
417 let mut u = projection.u;
418 if let Some(previous) = previous_u {
419 while u - previous > period * 0.5 {
420 u -= period;
421 }
422 while previous - u > period * 0.5 {
423 u += period;
424 }
425 }
426 previous_u = Some(u);
427 proj_u.push(u);
428 proj_v.push(projection.v);
429 point3.push(point);
430 params.push(section as f64 / DENSE as f64);
431 }
432 if closed_v {
439 for index in 2..=DENSE {
440 while proj_v[index] - proj_v[index - 1] > period_v * 0.5 {
441 proj_v[index] -= period_v;
442 }
443 while proj_v[index - 1] - proj_v[index] > period_v * 0.5 {
444 proj_v[index] += period_v;
445 }
446 }
447 while proj_v[0] - proj_v[1] > period_v * 0.5 {
448 proj_v[0] -= period_v;
449 }
450 while proj_v[1] - proj_v[0] > period_v * 0.5 {
451 proj_v[0] += period_v;
452 }
453 }
454 let mean_u: f64 = proj_u[1..DENSE].iter().sum::<f64>() / (DENSE - 1) as f64;
457 let seam_u = if mean_u - u0 > u1 - mean_u { u1 } else { u0 };
458 let pinned = |index: usize,
467 neighbour: usize,
468 proj_u: &[f64],
469 proj_v: &[f64]|
470 -> Result<(f64, f64), String> {
471 let point = point3[index];
472 let u_error = surface
473 .evaluate(seam_u, proj_v[index])?
474 .sub(point)
475 .length();
476 if closed_v {
477 let seam_v = nearest_seam_image(proj_v[neighbour], v0, v1);
478 let v_error = surface
479 .evaluate(proj_u[index], seam_v)?
480 .sub(point)
481 .length();
482 if v_error < u_error {
483 return Ok((proj_u[index], seam_v));
484 }
485 }
486 Ok((seam_u, proj_v[index]))
487 };
488 if start_is_crossing {
489 let (u, v) = pinned(0, 1, &proj_u, &proj_v)?;
490 proj_u[0] = u;
491 proj_v[0] = v;
492 }
493 if end_is_crossing {
494 let (u, v) = pinned(DENSE, DENSE - 1, &proj_u, &proj_v)?;
495 proj_u[DENSE] = u;
496 proj_v[DENSE] = v;
497 }
498 let mut shift = 0.0;
499 while mean_u + shift > u1 + 1e-9 {
500 shift -= period;
501 }
502 while mean_u + shift < u0 - 1e-9 {
503 shift += period;
504 }
505 for u in proj_u.iter_mut() {
506 *u += shift;
507 }
508 if closed_v {
509 let mean_v: f64 = proj_v[1..DENSE].iter().sum::<f64>() / (DENSE - 1) as f64;
510 let mut shift_v = 0.0;
511 while mean_v + shift_v > v1 + 1e-9 {
512 shift_v -= period_v;
513 }
514 while mean_v + shift_v < v0 - 1e-9 {
515 shift_v += period_v;
516 }
517 for v in proj_v.iter_mut() {
518 *v += shift_v;
519 }
520 }
521 let tolerance = 0.002;
524 let mut best: Option<NurbsCurve> = None;
525 for sections in [8usize, 12, 16, 24, 32, 48, 64, 96] {
526 if sections > DENSE {
527 break;
528 }
529 let indices: Vec<usize> = (0..=sections)
530 .map(|k| (k * DENSE / sections).min(DENSE))
531 .collect();
532 let points: Vec<Vec4> = indices
533 .iter()
534 .map(|&i| Vec4::from_point(Vec3::new(proj_u[i], proj_v[i], 0.0), 1.0))
535 .collect();
536 let knot_params: Vec<f64> = indices.iter().map(|&i| params[i]).collect();
537 let curve = fit::interpolate_homogeneous(&points, FIT_DEGREE, &knot_params)?;
538 let mut max_deviation: f64 = 0.0;
539 for i in 0..=DENSE {
540 let uv = curve.evaluate(params[i])?;
541 let on_surface = surface.evaluate(uv.x, uv.y)?;
542 max_deviation = max_deviation.max(on_surface.sub(point3[i]).length());
543 }
544 best = Some(curve);
545 if max_deviation <= tolerance {
546 break;
547 }
548 }
549 best.ok_or_else(|| "blend: piece pcurve projection failed".to_string())
550}