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