1use super::*;
2
3pub(crate) fn parameter_line(u0: f64, v0: f64, u1: f64, v1: f64) -> Result<NurbsCurve, String> {
4 make_line(Vec3::new(u0, v0, 0.0), Vec3::new(u1, v1, 0.0))
5}
6
7fn translated_curve(curve: &NurbsCurve, delta: Vec3) -> Result<NurbsCurve, String> {
8 NurbsCurve::new(
9 curve.degree,
10 curve.knots.clone(),
11 curve
12 .control_points
13 .iter()
14 .map(|point| Vec4 {
15 x: point.x + point.w * delta.x,
16 y: point.y + point.w * delta.y,
17 z: point.z + point.w * delta.z,
18 w: point.w,
19 })
20 .collect(),
21 )
22}
23
24pub(crate) fn curve_to_plane_parameters(
25 curve: &NurbsCurve,
26 origin: Vec3,
27 x_axis: Vec3,
28 y_axis: Vec3,
29) -> Result<NurbsCurve, String> {
30 NurbsCurve::new(
31 curve.degree,
32 curve.knots.clone(),
33 curve
34 .control_points
35 .iter()
36 .map(|point| {
37 let euclidean = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w);
38 let delta = euclidean.sub(origin);
39 Vec4 {
40 x: delta.dot(x_axis) * point.w,
41 y: delta.dot(y_axis) * point.w,
42 z: 0.0,
43 w: point.w,
44 }
45 })
46 .collect(),
47 )
48}
49
50pub(crate) fn profile_area(
51 curves: &[NurbsCurve],
52 origin: Vec3,
53 x_axis: Vec3,
54 y_axis: Vec3,
55) -> Result<f64, String> {
56 let mut area = 0.0;
57 for curve in curves {
58 let [start, end] = curve.domain()?;
59 let mut previous = curve.evaluate(start)?;
60 for index in 1..=64 {
61 let point = curve.evaluate(start + (end - start) * index as f64 / 64.0)?;
62 let a = previous.sub(origin);
63 let b = point.sub(origin);
64 area += 0.5 * (a.dot(x_axis) * b.dot(y_axis) - b.dot(x_axis) * a.dot(y_axis));
65 previous = point;
66 }
67 }
68 Ok(area)
69}
70
71pub fn extrude_profile_brep(
72 input_curves: &[NurbsCurve],
73 direction: Vec3,
74 distance: f64,
75) -> Result<BrepSolid, String> {
76 if input_curves.len() < 2 {
77 return Err("profile needs at least 2 curves".into());
78 }
79 if distance.abs() <= 1e-12 {
80 return Err("extrudeSolid: distance must be non-zero".into());
81 }
82 let displacement = direction.normalized()?.scale(distance);
83 let tolerance = 1e-6;
84 let mut curves: Vec<NurbsCurve> = input_curves
85 .iter()
86 .map(|curve| {
87 NurbsCurve::new(
88 curve.degree,
89 curve.knots.clone(),
90 curve.control_points.clone(),
91 )
92 })
93 .collect::<Result<_, _>>()?;
94
95 let mut samples = Vec::new();
96 for (index, curve) in curves.iter().enumerate() {
97 let [start, end] = curve.domain()?;
98 let next = &curves[(index + 1) % curves.len()];
99 let next_start = next.domain()?[0];
100 if curve
101 .evaluate(end)?
102 .sub(next.evaluate(next_start)?)
103 .length()
104 > tolerance
105 {
106 return Err(format!("profile not closed at curve {index}"));
107 }
108 for sample in 0..16 {
109 samples.push(curve.evaluate(start + (end - start) * sample as f64 / 16.0)?);
110 }
111 }
112 let mut normal = Vec3::default();
113 for index in 0..samples.len() {
114 let point = samples[index];
115 let next = samples[(index + 1) % samples.len()];
116 normal.x += (point.y - next.y) * (point.z + next.z);
117 normal.y += (point.z - next.z) * (point.x + next.x);
118 normal.z += (point.x - next.x) * (point.y + next.y);
119 }
120 normal = normal.normalized()?;
121 if normal.dot(displacement) < 0.0 {
122 normal = normal.scale(-1.0);
123 }
124 if normal.dot(displacement.normalized()?).abs() < 0.1 {
125 return Err("extrudeSolid: sweep direction is nearly parallel to profile plane".into());
126 }
127 let origin = samples[0];
128 if samples
129 .iter()
130 .any(|point| point.sub(origin).dot(normal).abs() > tolerance * 100.0)
131 {
132 return Err("extrudeSolid: profile is not planar".into());
133 }
134 let x_axis = normal.perpendicular()?;
135 let y_axis = normal.cross(x_axis).normalized()?;
136 let reversed_winding = profile_area(&curves, origin, x_axis, y_axis)? < 0.0;
137 if reversed_winding {
138 curves = curves
139 .iter()
140 .rev()
141 .map(NurbsCurve::reversed)
142 .collect::<Result<_, _>>()?;
143 }
144
145 let count = curves.len();
146 let points: Vec<Vec3> = curves
147 .iter()
148 .map(|curve| curve.domain().and_then(|domain| curve.evaluate(domain[0])))
149 .collect::<Result<_, _>>()?;
150 let mut vertices = Vec::with_capacity(count * 2);
151 for (index, point) in points.iter().enumerate() {
152 vertices.push(VertexRecord {
153 id: index as u64 + 1,
154 point: *point,
155 });
156 }
157 for (index, point) in points.iter().enumerate() {
158 vertices.push(VertexRecord {
159 id: (count + index) as u64 + 1,
160 point: point.add(displacement),
161 });
162 }
163
164 let mut edges = Vec::with_capacity(count * 3);
165 for (index, curve) in curves.iter().enumerate() {
166 let [start, end] = curve.domain()?;
167 edges.push(EdgeRecord {
168 id: 10 + index as u64,
169 curve: curve.clone(),
170 t0: start,
171 t1: end,
172 start_vertex_id: index as u64 + 1,
173 end_vertex_id: ((index + 1) % count) as u64 + 1,
174 degenerate: false,
175 name: None,
176 });
177 edges.push(EdgeRecord {
178 id: 10 + count as u64 + index as u64,
179 curve: translated_curve(curve, displacement)?,
180 t0: start,
181 t1: end,
182 start_vertex_id: (count + index) as u64 + 1,
183 end_vertex_id: (count + (index + 1) % count) as u64 + 1,
184 degenerate: false,
185 name: None,
186 });
187 edges.push(EdgeRecord {
188 id: 10 + 2 * count as u64 + index as u64,
189 curve: make_line(points[index], points[index].add(displacement))?,
190 t0: 0.0,
191 t1: 1.0,
192 start_vertex_id: index as u64 + 1,
193 end_vertex_id: (count + index) as u64 + 1,
194 degenerate: false,
195 name: None,
196 });
197 }
198
199 let mut next_id = 1000_u64;
200 let mut faces = Vec::with_capacity(count + 2);
201 for (index, curve) in curves.iter().enumerate() {
202 let [start, end] = curve.domain()?;
203 let coedges = vec![
204 CoedgeRecord {
205 id: next_id,
206 edge_id: 10 + index as u64,
207 forward: true,
208 pcurve: parameter_line(start, 0.0, end, 0.0)?,
209 },
210 CoedgeRecord {
211 id: next_id + 1,
212 edge_id: 10 + 2 * count as u64 + ((index + 1) % count) as u64,
213 forward: true,
214 pcurve: parameter_line(end, 0.0, end, 1.0)?,
215 },
216 CoedgeRecord {
217 id: next_id + 2,
218 edge_id: 10 + count as u64 + index as u64,
219 forward: false,
220 pcurve: parameter_line(end, 1.0, start, 1.0)?,
221 },
222 CoedgeRecord {
223 id: next_id + 3,
224 edge_id: 10 + 2 * count as u64 + index as u64,
225 forward: false,
226 pcurve: parameter_line(start, 1.0, start, 0.0)?,
227 },
228 ];
229 next_id += 4;
230 faces.push(FaceRecord {
231 id: next_id + 1,
232 surface: make_extrusion(curve, displacement)?,
233 same_sense: true,
234 loops: vec![LoopRecord {
235 id: next_id,
236 coedges,
237 }],
238 name: None,
239 });
240 next_id += 2;
241 }
242 if reversed_winding {
243 faces.reverse();
249 }
250
251 let mut min_x = f64::INFINITY;
252 let mut min_y = f64::INFINITY;
253 let mut max_x = f64::NEG_INFINITY;
254 let mut max_y = f64::NEG_INFINITY;
255 for point in &samples {
256 let delta = point.sub(origin);
257 min_x = min_x.min(delta.dot(x_axis));
258 max_x = max_x.max(delta.dot(x_axis));
259 min_y = min_y.min(delta.dot(y_axis));
260 max_y = max_y.max(delta.dot(y_axis));
261 }
262 let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
263 let width = max_x - min_x + 2.0 * padding;
264 let height = max_y - min_y + 2.0 * padding;
265 let bottom_origin = origin
266 .add(x_axis.scale(min_x - padding))
267 .add(y_axis.scale(min_y - padding));
268 let mut bottom_coedges = Vec::with_capacity(count);
269 for index in (0..count).rev() {
270 bottom_coedges.push(CoedgeRecord {
271 id: next_id,
272 edge_id: 10 + index as u64,
273 forward: false,
274 pcurve: curve_to_plane_parameters(&curves[index], bottom_origin, x_axis, y_axis)?
275 .reversed()?,
276 });
277 next_id += 1;
278 }
279 faces.push(FaceRecord {
280 id: next_id + 1,
281 surface: make_plane(bottom_origin, x_axis, y_axis, width, height)?,
282 same_sense: false,
283 loops: vec![LoopRecord {
284 id: next_id,
285 coedges: bottom_coedges,
286 }],
287 name: None,
288 });
289 next_id += 2;
290
291 let top_origin = bottom_origin.add(displacement);
292 let mut top_coedges = Vec::with_capacity(count);
293 for (index, curve) in curves.iter().enumerate() {
294 let translated = translated_curve(curve, displacement)?;
295 top_coedges.push(CoedgeRecord {
296 id: next_id,
297 edge_id: 10 + count as u64 + index as u64,
298 forward: true,
299 pcurve: curve_to_plane_parameters(&translated, top_origin, x_axis, y_axis)?,
300 });
301 next_id += 1;
302 }
303 faces.push(FaceRecord {
304 id: next_id + 1,
305 surface: make_plane(top_origin, x_axis, y_axis, width, height)?,
306 same_sense: true,
307 loops: vec![LoopRecord {
308 id: next_id,
309 coedges: top_coedges,
310 }],
311 name: None,
312 });
313 next_id += 2;
314
315 let solid = BrepSolid {
316 id: next_id + 1,
317 vertices,
318 edges,
319 shells: vec![ShellRecord { id: next_id, faces }],
320 genus: 0,
321 };
322 let issues = solid.validate();
323 if issues.is_empty() {
324 Ok(solid)
325 } else {
326 Err(format!(
327 "Rust extrusion builder produced invalid topology: {issues:?}"
328 ))
329 }
330}