1use std::collections::HashMap;
9
10use brepkit_math::vec::{Point3, Vec3};
11use brepkit_topology::Topology;
12use brepkit_topology::edge::{Edge, EdgeCurve};
13use brepkit_topology::face::{Face, FaceSurface};
14use brepkit_topology::shell::Shell;
15use brepkit_topology::solid::{Solid, SolidId};
16use brepkit_topology::vertex::Vertex;
17use brepkit_topology::wire::{OrientedEdge, Wire};
18
19use crate::IoError;
20
21pub fn read_step(input: &str, topo: &mut Topology) -> Result<Vec<SolidId>, IoError> {
32 let entities = parse_step_entities(input)?;
33 let mut builder = StepBuilder::new(topo, &entities);
34 builder.build_all_solids()
35}
36
37#[derive(Debug)]
41struct StepEntity {
42 entity_type: String,
43 attrs: String,
44}
45
46fn parse_step_entities(input: &str) -> Result<HashMap<u64, StepEntity>, IoError> {
48 let mut entities = HashMap::new();
49
50 let data_start = input.find("DATA;").ok_or_else(|| IoError::ParseError {
51 reason: "no DATA section found".to_string(),
52 })?;
53 let data_end = input[data_start..]
54 .find("ENDSEC;")
55 .ok_or_else(|| IoError::ParseError {
56 reason: "no ENDSEC after DATA".to_string(),
57 })?;
58
59 let data_section = &input[data_start + 5..data_start + data_end];
60 let joined = data_section.replace(['\n', '\r'], " ");
61
62 for statement in joined.split(';') {
63 let stmt = statement.trim();
64 if stmt.is_empty() {
65 continue;
66 }
67
68 if let Some(eq_pos) = stmt.find('=') {
69 let id_part = stmt[..eq_pos].trim();
70 let rest = stmt[eq_pos + 1..].trim();
71
72 if let Some(id) = parse_entity_id(id_part)
73 && let Some(paren_pos) = rest.find('(')
74 {
75 let entity_type = rest[..paren_pos].trim().to_uppercase();
76 let attrs = rest[paren_pos + 1..].trim();
79
80 entities.insert(
81 id,
82 StepEntity {
83 entity_type,
84 attrs: attrs.to_string(),
85 },
86 );
87 }
88 }
89 }
90
91 Ok(entities)
92}
93
94fn parse_entity_id(s: &str) -> Option<u64> {
96 let trimmed = s.trim();
97 trimmed.strip_prefix('#')?.parse().ok()
98}
99
100struct StepBuilder<'a> {
104 topo: &'a mut Topology,
105 entities: &'a HashMap<u64, StepEntity>,
106 vertex_cache: HashMap<u64, brepkit_topology::vertex::VertexId>,
107 edge_cache: HashMap<u64, brepkit_topology::edge::EdgeId>,
108}
109
110impl<'a> StepBuilder<'a> {
111 fn new(topo: &'a mut Topology, entities: &'a HashMap<u64, StepEntity>) -> Self {
112 Self {
113 topo,
114 entities,
115 vertex_cache: HashMap::new(),
116 edge_cache: HashMap::new(),
117 }
118 }
119
120 fn build_all_solids(&mut self) -> Result<Vec<SolidId>, IoError> {
121 let brep_ids: Vec<u64> = self
122 .entities
123 .iter()
124 .filter(|(_, e)| e.entity_type == "MANIFOLD_SOLID_BREP")
125 .map(|(&id, _)| id)
126 .collect();
127
128 let mut solid_ids = Vec::new();
129 for brep_id in brep_ids {
130 let solid_id = self.build_solid(brep_id)?;
131 solid_ids.push(solid_id);
132 }
133 Ok(solid_ids)
134 }
135
136 fn build_solid(&mut self, brep_id: u64) -> Result<SolidId, IoError> {
137 let attrs = self.get_entity(brep_id)?.attrs.clone();
138 let refs = parse_refs(&attrs);
139 let shell_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
141 reason: format!("MANIFOLD_SOLID_BREP #{brep_id} missing shell reference"),
142 })?;
143
144 let shell_id = self.build_shell(shell_ref)?;
145 let solid_id = self.topo.add_solid(Solid::new(shell_id, Vec::new()));
146 Ok(solid_id)
147 }
148
149 fn build_shell(&mut self, shell_ref: u64) -> Result<brepkit_topology::shell::ShellId, IoError> {
150 let attrs = self.get_entity(shell_ref)?.attrs.clone();
151 let face_refs = parse_list_refs(&attrs);
152
153 let mut face_ids = Vec::new();
154 for face_ref in face_refs {
155 let face_id = self.build_face(face_ref)?;
156 face_ids.push(face_id);
157 }
158
159 let shell = Shell::new(face_ids).map_err(|e| IoError::ParseError {
160 reason: format!("failed to build shell from STEP: {e}"),
161 })?;
162 let shell_id = self.topo.add_shell(shell);
163 Ok(shell_id)
164 }
165
166 #[allow(clippy::too_many_lines)]
167 fn build_face(&mut self, face_ref: u64) -> Result<brepkit_topology::face::FaceId, IoError> {
168 let attrs = self.get_entity(face_ref)?.attrs.clone();
169 let orient_tail = attrs.trim_end_matches(')').trim();
171 let face_reversed = orient_tail.ends_with(".F.") || orient_tail.ends_with(".FALSE.");
172 let all_refs = parse_refs(&attrs);
173 let list_refs = parse_list_refs(&attrs);
174
175 let list_set: std::collections::HashSet<u64> = list_refs.iter().copied().collect();
177 let surface_ref = all_refs
178 .iter()
179 .rev()
180 .find(|r| !list_set.contains(r))
181 .copied()
182 .ok_or_else(|| IoError::ParseError {
183 reason: format!("ADVANCED_FACE #{face_ref} missing surface reference"),
184 })?;
185
186 let surface = self.build_surface(surface_ref)?;
187
188 let mut outer_wire = None;
189 let mut inner_wires = Vec::new();
190
191 for &bound_ref in &list_refs {
192 let bound_entity = self.get_entity(bound_ref)?;
193 let is_outer = bound_entity.entity_type == "FACE_OUTER_BOUND";
194 let bound_attrs = bound_entity.attrs.clone();
195 let bound_refs = parse_refs(&bound_attrs);
196
197 if let Some(&loop_ref) = bound_refs.first() {
198 let wire_id = self.build_edge_loop(loop_ref)?;
199 if is_outer && outer_wire.is_none() {
200 outer_wire = Some(wire_id);
201 } else {
202 inner_wires.push(wire_id);
203 }
204 }
205 }
206
207 let outer = outer_wire.or_else(|| {
209 if inner_wires.is_empty() {
210 None
211 } else {
212 Some(inner_wires.remove(0))
213 }
214 });
215
216 let outer = outer.ok_or_else(|| IoError::ParseError {
217 reason: format!("ADVANCED_FACE #{face_ref} has no bounds"),
218 })?;
219
220 let face_id = if face_reversed {
221 self.topo
222 .add_face(Face::new_reversed(outer, inner_wires, surface))
223 } else {
224 self.topo.add_face(Face::new(outer, inner_wires, surface))
225 };
226 Ok(face_id)
227 }
228
229 fn build_surface(&self, surface_ref: u64) -> Result<FaceSurface, IoError> {
230 let entity = self.get_entity(surface_ref)?;
231 let entity_type = entity.entity_type.clone();
232 let attrs = entity.attrs.clone();
233
234 match entity_type.as_str() {
235 "PLANE" => {
236 let refs = parse_refs(&attrs);
237 let axis_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
238 reason: format!("PLANE #{surface_ref} missing axis reference"),
239 })?;
240 let (origin, normal, _ref_dir) = self.build_axis2_placement(axis_ref)?;
241 let d = normal.dot(Vec3::new(origin.x(), origin.y(), origin.z()));
242 Ok(FaceSurface::Plane { normal, d })
243 }
244 "CYLINDRICAL_SURFACE" => {
245 let refs = parse_refs(&attrs);
246 let floats = parse_floats(&attrs);
247 let axis_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
248 reason: format!("CYLINDRICAL_SURFACE #{surface_ref} missing axis"),
249 })?;
250 let radius = floats.first().copied().ok_or_else(|| IoError::ParseError {
251 reason: format!("CYLINDRICAL_SURFACE #{surface_ref} missing radius"),
252 })?;
253 let (origin, axis, _ref_dir) = self.build_axis2_placement(axis_ref)?;
254 let cyl = brepkit_math::surfaces::CylindricalSurface::new(origin, axis, radius)
255 .map_err(|e| IoError::ParseError {
256 reason: format!("CYLINDRICAL_SURFACE #{surface_ref}: {e}"),
257 })?;
258 Ok(FaceSurface::Cylinder(cyl))
259 }
260 "CONICAL_SURFACE" => {
261 let refs = parse_refs(&attrs);
262 let floats = parse_floats(&attrs);
263 let axis_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
264 reason: format!("CONICAL_SURFACE #{surface_ref} missing axis"),
265 })?;
266 let half_angle = floats.last().copied().ok_or_else(|| IoError::ParseError {
269 reason: format!("CONICAL_SURFACE #{surface_ref} missing half_angle"),
270 })?;
271 let (apex, axis, _ref_dir) = self.build_axis2_placement(axis_ref)?;
272 let cone = brepkit_math::surfaces::ConicalSurface::new(apex, axis, half_angle)
273 .map_err(|e| IoError::ParseError {
274 reason: format!("CONICAL_SURFACE #{surface_ref}: {e}"),
275 })?;
276 Ok(FaceSurface::Cone(cone))
277 }
278 "SPHERICAL_SURFACE" => {
279 let refs = parse_refs(&attrs);
280 let floats = parse_floats(&attrs);
281 let axis_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
282 reason: format!("SPHERICAL_SURFACE #{surface_ref} missing axis"),
283 })?;
284 let radius = floats.first().copied().ok_or_else(|| IoError::ParseError {
285 reason: format!("SPHERICAL_SURFACE #{surface_ref} missing radius"),
286 })?;
287 let (center, _axis, _ref_dir) = self.build_axis2_placement(axis_ref)?;
288 let sphere = brepkit_math::surfaces::SphericalSurface::new(center, radius)
289 .map_err(|e| IoError::ParseError {
290 reason: format!("SPHERICAL_SURFACE #{surface_ref}: {e}"),
291 })?;
292 Ok(FaceSurface::Sphere(sphere))
293 }
294 "TOROIDAL_SURFACE" => {
295 let refs = parse_refs(&attrs);
296 let floats = parse_floats(&attrs);
297 let axis_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
298 reason: format!("TOROIDAL_SURFACE #{surface_ref} missing axis"),
299 })?;
300 let major_r = floats.first().copied().ok_or_else(|| IoError::ParseError {
301 reason: format!("TOROIDAL_SURFACE #{surface_ref} missing major_radius"),
302 })?;
303 let minor_r = floats.get(1).copied().ok_or_else(|| IoError::ParseError {
304 reason: format!("TOROIDAL_SURFACE #{surface_ref} missing minor_radius"),
305 })?;
306 let (center, axis, ref_dir) = self.build_axis2_placement(axis_ref)?;
307 let torus = brepkit_math::surfaces::ToroidalSurface::with_axis_and_ref_dir(
308 center, major_r, minor_r, axis, ref_dir,
309 )
310 .map_err(|e| IoError::ParseError {
311 reason: format!("TOROIDAL_SURFACE #{surface_ref}: {e}"),
312 })?;
313 Ok(FaceSurface::Torus(torus))
314 }
315 "B_SPLINE_SURFACE_WITH_KNOTS" | "BOUNDED_SURFACE" | "B_SPLINE_SURFACE" => {
316 let is_rational = attrs.contains("RATIONAL");
317 self.build_bspline_surface(surface_ref, &attrs, is_rational)
318 }
319 _ if entity_type.is_empty() || attrs.contains("B_SPLINE_SURFACE_WITH_KNOTS") => {
320 let is_rational = attrs.contains("RATIONAL");
321 let bspline_attrs = find_composite_bspline_attrs(&attrs, "B_SPLINE_SURFACE")
322 .ok_or_else(|| IoError::UnsupportedEntity {
323 entity: format!("composite surface #{surface_ref}"),
324 })?;
325 self.build_bspline_surface(surface_ref, bspline_attrs, is_rational)
326 }
327 _ => Err(IoError::UnsupportedEntity {
328 entity: entity_type,
329 }),
330 }
331 }
332
333 fn build_edge_loop(
334 &mut self,
335 loop_ref: u64,
336 ) -> Result<brepkit_topology::wire::WireId, IoError> {
337 let attrs = self.get_entity(loop_ref)?.attrs.clone();
338 let oe_refs = parse_list_refs(&attrs);
339
340 let mut oriented_edges = Vec::new();
341 for oe_ref in oe_refs {
342 let oe = self.build_oriented_edge(oe_ref)?;
343 oriented_edges.push(oe);
344 }
345
346 let wire = Wire::new(oriented_edges, true).map_err(|e| IoError::ParseError {
347 reason: format!("failed to create wire from edge loop #{loop_ref}: {e}"),
348 })?;
349 let wire_id = self.topo.add_wire(wire);
350 Ok(wire_id)
351 }
352
353 fn build_oriented_edge(&mut self, oe_ref: u64) -> Result<OrientedEdge, IoError> {
354 let attrs = self.get_entity(oe_ref)?.attrs.clone();
355 let refs = parse_refs(&attrs);
356 let forward = attrs.contains(".T.");
357
358 let edge_curve_ref = refs.last().copied().ok_or_else(|| IoError::ParseError {
359 reason: format!("ORIENTED_EDGE #{oe_ref} missing edge curve reference"),
360 })?;
361
362 let edge_id = self.build_edge_curve(edge_curve_ref)?;
363 Ok(OrientedEdge::new(edge_id, forward))
364 }
365
366 fn build_edge_curve(&mut self, ec_ref: u64) -> Result<brepkit_topology::edge::EdgeId, IoError> {
367 if let Some(&cached) = self.edge_cache.get(&ec_ref) {
368 return Ok(cached);
369 }
370
371 let attrs = self.get_entity(ec_ref)?.attrs.clone();
372 let refs = parse_refs(&attrs);
373 if refs.len() < 3 {
374 return Err(IoError::ParseError {
375 reason: format!("EDGE_CURVE #{ec_ref} needs at least 3 references"),
376 });
377 }
378
379 let start_vp = self.build_vertex_point(refs[0])?;
380 let end_vp = self.build_vertex_point(refs[1])?;
381
382 let curve = self.build_curve_geometry(refs[2])?;
383
384 let edge_id = self.topo.add_edge(Edge::new(start_vp, end_vp, curve));
385
386 self.edge_cache.insert(ec_ref, edge_id);
387 Ok(edge_id)
388 }
389
390 fn build_curve_geometry(&self, curve_ref: u64) -> Result<EdgeCurve, IoError> {
395 let entity = self.get_entity(curve_ref)?;
396 let entity_type = entity.entity_type.clone();
397 let attrs = entity.attrs.clone();
398
399 match entity_type.as_str() {
400 "LINE" => Ok(EdgeCurve::Line),
401 "CIRCLE" => {
402 let refs = parse_refs(&attrs);
403 let floats = parse_floats(&attrs);
404 let axis_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
405 reason: format!("CIRCLE #{curve_ref} missing axis reference"),
406 })?;
407 let radius = floats.first().copied().ok_or_else(|| IoError::ParseError {
408 reason: format!("CIRCLE #{curve_ref} missing radius"),
409 })?;
410 let (center, normal, _u_axis) = self.build_axis2_placement(axis_ref)?;
411 let circle =
412 brepkit_math::curves::Circle3D::new(center, normal, radius).map_err(|e| {
413 IoError::ParseError {
414 reason: format!("CIRCLE #{curve_ref}: {e}"),
415 }
416 })?;
417 Ok(EdgeCurve::Circle(circle))
418 }
419 "ELLIPSE" => {
420 let refs = parse_refs(&attrs);
421 let floats = parse_floats(&attrs);
422 let axis_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
423 reason: format!("ELLIPSE #{curve_ref} missing axis reference"),
424 })?;
425 if floats.len() < 2 {
426 return Err(IoError::ParseError {
427 reason: format!("ELLIPSE #{curve_ref} needs semi_major and semi_minor"),
428 });
429 }
430 let (center, normal, _u_axis) = self.build_axis2_placement(axis_ref)?;
431 let ellipse =
432 brepkit_math::curves::Ellipse3D::new(center, normal, floats[0], floats[1])
433 .map_err(|e| IoError::ParseError {
434 reason: format!("ELLIPSE #{curve_ref}: {e}"),
435 })?;
436 Ok(EdgeCurve::Ellipse(ellipse))
437 }
438 "B_SPLINE_CURVE_WITH_KNOTS" => self.build_bspline_curve(curve_ref, &attrs, false),
439 _ if entity_type.is_empty() || attrs.contains("B_SPLINE_CURVE_WITH_KNOTS") => {
440 let is_rational = attrs.contains("RATIONAL");
441 let bspline_attrs = find_composite_bspline_attrs(&attrs, "B_SPLINE_CURVE")
442 .ok_or_else(|| IoError::UnsupportedEntity {
443 entity: format!("composite curve #{curve_ref}"),
444 })?;
445 self.build_bspline_curve(curve_ref, bspline_attrs, is_rational)
446 }
447 _ => Err(IoError::UnsupportedEntity {
448 entity: format!("{entity_type} (curve #{curve_ref})"),
449 }),
450 }
451 }
452
453 fn build_bspline_curve(
457 &self,
458 curve_ref: u64,
459 attrs: &str,
460 is_rational: bool,
461 ) -> Result<EdgeCurve, IoError> {
462 let parsed = parse_bspline_curve_attrs(attrs).ok_or_else(|| IoError::ParseError {
463 reason: format!("B_SPLINE_CURVE #{curve_ref} could not parse attributes"),
464 })?;
465 let (degree, cp_refs, mults, knot_vals) = parsed;
466
467 let mut control_points = Vec::with_capacity(cp_refs.len());
468 for &cp_ref in &cp_refs {
469 control_points.push(self.build_cartesian_point(cp_ref)?);
470 }
471
472 let knots = expand_knots(&mults, &knot_vals);
473
474 let weights = if is_rational {
476 extract_rational_weights(attrs, control_points.len())
477 } else {
478 vec![1.0; control_points.len()]
479 };
480
481 let nurbs = brepkit_math::nurbs::NurbsCurve::new(degree, knots, control_points, weights)
482 .map_err(|e| IoError::ParseError {
483 reason: format!("B_SPLINE_CURVE #{curve_ref}: {e}"),
484 })?;
485 Ok(EdgeCurve::NurbsCurve(nurbs))
486 }
487
488 fn build_bspline_surface(
490 &self,
491 surface_ref: u64,
492 attrs: &str,
493 is_rational: bool,
494 ) -> Result<FaceSurface, IoError> {
495 let parsed = parse_bspline_surface_attrs(attrs).ok_or_else(|| IoError::ParseError {
496 reason: format!("B_SPLINE_SURFACE #{surface_ref} could not parse attributes"),
497 })?;
498 let (degree_u, degree_v, cp_grid_refs, u_mults, v_mults, u_knots, v_knots) = parsed;
499
500 let mut cp_grid: Vec<Vec<Point3>> = Vec::new();
501 for row_refs in &cp_grid_refs {
502 let mut row: Vec<Point3> = Vec::new();
503 for &cp_ref in row_refs {
504 row.push(self.build_cartesian_point(cp_ref)?);
505 }
506 cp_grid.push(row);
507 }
508
509 let knots_u = expand_knots(&u_mults, &u_knots);
510 let knots_v = expand_knots(&v_mults, &v_knots);
511
512 let n_rows = cp_grid.len();
513 let n_cols = cp_grid.first().map_or(0, Vec::len);
514
515 let weights = if is_rational {
516 extract_rational_weight_grid(attrs, n_rows, n_cols)
517 } else {
518 vec![vec![1.0; n_cols]; n_rows]
519 };
520
521 let nurbs = brepkit_math::nurbs::NurbsSurface::new(
522 degree_u, degree_v, knots_u, knots_v, cp_grid, weights,
523 )
524 .map_err(|e| IoError::ParseError {
525 reason: format!("B_SPLINE_SURFACE #{surface_ref}: {e}"),
526 })?;
527 Ok(FaceSurface::Nurbs(nurbs))
528 }
529
530 fn build_vertex_point(
531 &mut self,
532 vp_ref: u64,
533 ) -> Result<brepkit_topology::vertex::VertexId, IoError> {
534 if let Some(&cached) = self.vertex_cache.get(&vp_ref) {
535 return Ok(cached);
536 }
537
538 let attrs = self.get_entity(vp_ref)?.attrs.clone();
539 let refs = parse_refs(&attrs);
540 let cp_ref = refs.first().copied().ok_or_else(|| IoError::ParseError {
541 reason: format!("VERTEX_POINT #{vp_ref} missing point reference"),
542 })?;
543
544 let point = self.build_cartesian_point(cp_ref)?;
545 let vid = self.topo.add_vertex(Vertex::new(point, 1e-7));
546
547 self.vertex_cache.insert(vp_ref, vid);
548 Ok(vid)
549 }
550
551 fn build_cartesian_point(&self, cp_ref: u64) -> Result<Point3, IoError> {
552 let attrs = &self.get_entity(cp_ref)?.attrs;
553 let coords = parse_floats(attrs);
554 if coords.len() < 3 {
555 return Err(IoError::ParseError {
556 reason: format!(
557 "CARTESIAN_POINT #{cp_ref} needs 3 coordinates, got {}",
558 coords.len()
559 ),
560 });
561 }
562 Ok(Point3::new(coords[0], coords[1], coords[2]))
563 }
564
565 fn build_direction(&self, dir_ref: u64) -> Result<Vec3, IoError> {
566 let attrs = &self.get_entity(dir_ref)?.attrs;
567 let coords = parse_floats(attrs);
568 if coords.len() < 3 {
569 return Err(IoError::ParseError {
570 reason: format!(
571 "DIRECTION #{dir_ref} needs 3 components, got {}",
572 coords.len()
573 ),
574 });
575 }
576 Ok(Vec3::new(coords[0], coords[1], coords[2]))
577 }
578
579 fn build_axis2_placement(&self, axis_ref: u64) -> Result<(Point3, Vec3, Vec3), IoError> {
580 let attrs = self.get_entity(axis_ref)?.attrs.clone();
581 let refs = parse_refs(&attrs);
582 if refs.len() < 3 {
583 return Err(IoError::ParseError {
584 reason: format!("AXIS2_PLACEMENT_3D #{axis_ref} needs 3 sub-references"),
585 });
586 }
587 let origin = self.build_cartesian_point(refs[0])?;
588 let axis = self.build_direction(refs[1])?;
589 let ref_dir = self.build_direction(refs[2])?;
590 Ok((origin, axis, ref_dir))
591 }
592
593 fn get_entity(&self, id: u64) -> Result<&StepEntity, IoError> {
594 self.entities.get(&id).ok_or_else(|| IoError::ParseError {
595 reason: format!("entity #{id} not found"),
596 })
597 }
598}
599
600fn parse_refs(attrs: &str) -> Vec<u64> {
604 let mut refs = Vec::new();
605 let mut i = 0;
606 let bytes = attrs.as_bytes();
607 while i < bytes.len() {
608 if bytes[i] == b'#' {
609 i += 1;
610 let start = i;
611 while i < bytes.len() && bytes[i].is_ascii_digit() {
612 i += 1;
613 }
614 if i > start
615 && let Ok(num) = attrs[start..i].parse::<u64>()
616 {
617 refs.push(num);
618 }
619 } else {
620 i += 1;
621 }
622 }
623 refs
624}
625
626fn parse_list_refs(attrs: &str) -> Vec<u64> {
628 if let Some(start) = attrs.find('(')
629 && let Some(end) = attrs[start..].find(')')
630 {
631 let inner = &attrs[start + 1..start + end];
632 return parse_refs(inner);
633 }
634 Vec::new()
635}
636
637fn parse_floats(attrs: &str) -> Vec<f64> {
641 let mut result = Vec::new();
642 if let Some(start) = attrs.find('(')
644 && let Some(end) = attrs[start..].find(')')
645 {
646 let inner = &attrs[start + 1..start + end];
647 for part in inner.split(',') {
648 let trimmed = part.trim();
649 if let Ok(v) = trimmed.parse::<f64>() {
650 result.push(v);
651 }
652 }
653 }
654 if result.is_empty() {
656 for part in attrs.split(',') {
657 let trimmed = part.trim().trim_matches('\'').trim_end_matches(')');
658 if trimmed.starts_with('#') || trimmed.starts_with('.') || trimmed.is_empty() {
659 continue;
660 }
661 if let Ok(v) = trimmed.parse::<f64>() {
662 result.push(v);
663 }
664 }
665 }
666 result
667}
668
669fn find_composite_bspline_attrs<'a>(attrs: &'a str, base_name: &str) -> Option<&'a str> {
674 let with_knots = format!("{base_name}_WITH_KNOTS");
675 if let Some(pos) = attrs.find(&with_knots) {
676 return Some(&attrs[pos + with_knots.len()..]);
677 }
678 let anchored = format!("{base_name}(");
681 if let Some(pos) = attrs.find(&anchored) {
682 return Some(&attrs[pos + base_name.len()..]);
683 }
684 None
685}
686
687fn parse_ints_in_parens(s: &str) -> Vec<u32> {
689 let mut result = Vec::new();
690 for part in s.split(',') {
691 let trimmed = part.trim().trim_matches('(').trim_matches(')').trim();
692 if let Ok(v) = trimmed.parse::<u32>() {
693 result.push(v);
694 }
695 }
696 result
697}
698
699fn extract_rational_weights(attrs: &str, expected_count: usize) -> Vec<f64> {
705 let marker = if attrs.contains("RATIONAL_B_SPLINE_SURFACE") {
706 "RATIONAL_B_SPLINE_SURFACE"
707 } else {
708 "RATIONAL_B_SPLINE_CURVE"
709 };
710
711 if let Some(pos) = attrs.find(marker) {
712 let after = &attrs[pos + marker.len()..];
713 if let Some(paren_start) = after.find('(') {
714 let rest = &after[paren_start + 1..];
715 let weights = parse_weight_list(rest);
716 if weights.len() >= expected_count {
717 return weights[..expected_count].to_vec();
718 }
719 }
722 }
723
724 vec![1.0; expected_count]
725}
726
727fn parse_weight_list(s: &str) -> Vec<f64> {
731 let mut weights = Vec::new();
732 let mut depth = 0i32;
733 let mut current = String::new();
734
735 for ch in s.chars() {
736 match ch {
737 '(' => {
738 depth += 1;
739 }
740 ')' => {
741 depth -= 1;
742 if depth < 0 {
743 let trimmed = current.trim();
745 if let Ok(v) = trimmed.parse::<f64>() {
746 weights.push(v);
747 }
748 break;
749 }
750 }
751 ',' if depth <= 1 => {
752 let trimmed = current.trim();
753 if let Ok(v) = trimmed.parse::<f64>() {
754 weights.push(v);
755 }
756 current.clear();
757 continue;
758 }
759 ',' => {
760 let trimmed = current.trim();
763 if let Ok(v) = trimmed.parse::<f64>() {
764 weights.push(v);
765 }
766 current.clear();
767 continue;
768 }
769 _ => {}
770 }
771 if depth >= 0 && ch != '(' && ch != ')' {
772 current.push(ch);
773 }
774 }
775
776 weights
777}
778
779fn extract_rational_weight_grid(attrs: &str, n_rows: usize, n_cols: usize) -> Vec<Vec<f64>> {
782 let flat = extract_rational_weights(attrs, n_rows * n_cols);
783 let tol = brepkit_math::tolerance::Tolerance::new();
784 if flat.len() == n_rows * n_cols && flat.iter().any(|&w| !tol.approx_eq(w, 1.0)) {
785 flat.chunks(n_cols).map(<[f64]>::to_vec).collect()
786 } else {
787 vec![vec![1.0; n_cols]; n_rows]
788 }
789}
790
791#[allow(clippy::type_complexity)]
798fn parse_bspline_surface_attrs(
799 attrs: &str,
800) -> Option<(
801 usize,
802 usize,
803 Vec<Vec<u64>>,
804 Vec<u32>,
805 Vec<u32>,
806 Vec<f64>,
807 Vec<f64>,
808)> {
809 let mut tokens = Vec::new();
814 let mut depth = 0i32;
815 let mut current = String::new();
816 let mut groups: Vec<String> = Vec::new();
817
818 for ch in attrs.chars() {
819 match ch {
820 '(' => {
821 if depth == 0 && !current.trim().is_empty() {
822 tokens.push(current.trim().to_string());
823 current.clear();
824 }
825 depth += 1;
826 current.push(ch);
827 }
828 ')' => {
829 current.push(ch);
830 depth -= 1;
831 if depth == 0 {
832 groups.push(current.clone());
833 current.clear();
834 }
835 }
836 ',' if depth == 0 => {
837 let trimmed = current.trim().to_string();
838 if !trimmed.is_empty() {
839 tokens.push(trimmed);
840 }
841 current.clear();
842 }
843 _ => {
844 current.push(ch);
845 }
846 }
847 }
848 if !current.trim().is_empty() {
849 tokens.push(current.trim().to_string());
850 }
851
852 let mut degrees: Vec<usize> = Vec::new();
857 for tok in &tokens {
858 if tok.starts_with('\'') || tok.starts_with('.') {
859 continue;
860 }
861 if let Ok(d) = tok.parse::<usize>() {
862 degrees.push(d);
863 }
864 }
865
866 if degrees.len() < 2 {
867 return None;
868 }
869 let degree_u = degrees[0];
870 let degree_v = degrees[1];
871
872 if groups.len() < 5 {
879 return None;
880 }
881
882 let cp_grid = parse_nested_refs(&groups[0]);
884
885 let u_mults = parse_ints_in_parens(&groups[1]);
886 let v_mults = parse_ints_in_parens(&groups[2]);
887
888 let u_knots = parse_floats(&groups[3]);
889 let v_knots = parse_floats(&groups[4]);
890
891 Some((
892 degree_u, degree_v, cp_grid, u_mults, v_mults, u_knots, v_knots,
893 ))
894}
895
896fn parse_nested_refs(s: &str) -> Vec<Vec<u64>> {
898 let mut rows: Vec<Vec<u64>> = Vec::new();
899 let mut depth = 0i32;
900 let mut current = String::new();
901
902 for ch in s.chars() {
903 match ch {
904 '(' => {
905 depth += 1;
906 if depth >= 2 {
907 current.push(ch);
908 }
909 }
910 ')' => {
911 if depth >= 2 {
912 current.push(ch);
913 }
914 depth -= 1;
915 if depth == 1 && !current.is_empty() {
916 rows.push(parse_refs(¤t));
918 current.clear();
919 }
920 }
921 ',' if depth == 1 => {
922 if !current.is_empty() {
924 rows.push(parse_refs(¤t));
925 current.clear();
926 }
927 }
928 _ => {
929 if depth >= 2 {
930 current.push(ch);
931 }
932 }
933 }
934 }
935
936 rows
937}
938
939fn expand_knots(mults: &[u32], vals: &[f64]) -> Vec<f64> {
944 let mut knots = Vec::new();
945 for (&m, &v) in mults.iter().zip(vals.iter()) {
946 for _ in 0..m {
947 knots.push(v);
948 }
949 }
950 knots
951}
952
953#[allow(clippy::type_complexity)]
959fn parse_bspline_curve_attrs(attrs: &str) -> Option<(usize, Vec<u64>, Vec<u32>, Vec<f64>)> {
960 let mut tokens = Vec::new();
961 let mut depth = 0i32;
962 let mut current = String::new();
963 let mut groups: Vec<String> = Vec::new();
964
965 for ch in attrs.chars() {
966 match ch {
967 '(' => {
968 if depth == 0 && !current.trim().is_empty() {
969 tokens.push(current.trim().to_string());
970 current.clear();
971 }
972 depth += 1;
973 current.push(ch);
974 }
975 ')' => {
976 current.push(ch);
977 depth -= 1;
978 if depth == 0 {
979 groups.push(current.clone());
980 current.clear();
981 }
982 }
983 ',' if depth == 0 => {
984 let trimmed = current.trim().to_string();
985 if !trimmed.is_empty() {
986 tokens.push(trimmed);
987 }
988 current.clear();
989 }
990 _ => {
991 current.push(ch);
992 }
993 }
994 }
995 if !current.trim().is_empty() {
996 tokens.push(current.trim().to_string());
997 }
998
999 let mut degree = None;
1000 for tok in &tokens {
1001 if tok.starts_with('\'') || tok.starts_with('.') {
1002 continue;
1003 }
1004 if let Ok(d) = tok.parse::<usize>() {
1005 degree = Some(d);
1006 break;
1007 }
1008 }
1009 let degree = degree?;
1010
1011 if groups.len() < 3 {
1013 return None;
1014 }
1015
1016 let cp_refs = parse_refs(&groups[0]);
1017 let mults = parse_ints_in_parens(&groups[1]);
1018 let knots = parse_floats(&groups[2]);
1019
1020 Some((degree, cp_refs, mults, knots))
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 #![allow(clippy::unwrap_used, clippy::expect_used)]
1026
1027 use brepkit_topology::Topology;
1028 use brepkit_topology::test_utils::make_unit_cube_non_manifold;
1029
1030 use super::*;
1031 use crate::step::writer;
1032
1033 #[test]
1034 fn roundtrip_unit_cube() {
1035 let mut write_topo = Topology::new();
1036 let solid = make_unit_cube_non_manifold(&mut write_topo);
1037
1038 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1039
1040 let mut read_topo = Topology::new();
1041 let solids = read_step(&step_str, &mut read_topo).unwrap();
1042
1043 assert_eq!(solids.len(), 1);
1044
1045 let read_solid = read_topo.solid(solids[0]).unwrap();
1046 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1047 assert_eq!(shell.faces().len(), 6);
1048 }
1049
1050 #[test]
1051 fn roundtrip_box_primitive() {
1052 let mut write_topo = Topology::new();
1053 let solid =
1054 brepkit_operations::primitives::make_box(&mut write_topo, 2.0, 3.0, 4.0).unwrap();
1055
1056 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1057
1058 let mut read_topo = Topology::new();
1059 let solids = read_step(&step_str, &mut read_topo).unwrap();
1060
1061 assert_eq!(solids.len(), 1);
1062 let read_solid = read_topo.solid(solids[0]).unwrap();
1063 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1064 assert_eq!(shell.faces().len(), 6);
1065 }
1066
1067 #[test]
1068 fn roundtrip_multiple_solids() {
1069 let mut write_topo = Topology::new();
1070 let s1 = brepkit_operations::primitives::make_box(&mut write_topo, 1.0, 1.0, 1.0).unwrap();
1071 let s2 = make_unit_cube_non_manifold(&mut write_topo);
1072
1073 let step_str = writer::write_step(&write_topo, &[s1, s2]).unwrap();
1074
1075 let mut read_topo = Topology::new();
1076 let solids = read_step(&step_str, &mut read_topo).unwrap();
1077
1078 assert_eq!(solids.len(), 2);
1079 }
1080
1081 #[test]
1082 fn roundtrip_faces_have_wires() {
1083 let mut write_topo = Topology::new();
1084 let solid = make_unit_cube_non_manifold(&mut write_topo);
1085
1086 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1087
1088 let mut read_topo = Topology::new();
1089 let solids = read_step(&step_str, &mut read_topo).unwrap();
1090
1091 let read_solid = read_topo.solid(solids[0]).unwrap();
1092 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1093
1094 for &face_id in shell.faces() {
1095 let face = read_topo.face(face_id).unwrap();
1096 let wire = read_topo.wire(face.outer_wire()).unwrap();
1097 assert_eq!(wire.edges().len(), 4, "cube face should have 4 edges");
1098 }
1099 }
1100
1101 #[test]
1102 fn roundtrip_faces_are_planar() {
1103 let mut write_topo = Topology::new();
1104 let solid = make_unit_cube_non_manifold(&mut write_topo);
1105
1106 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1107
1108 let mut read_topo = Topology::new();
1109 let solids = read_step(&step_str, &mut read_topo).unwrap();
1110
1111 let read_solid = read_topo.solid(solids[0]).unwrap();
1112 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1113
1114 for &face_id in shell.faces() {
1115 let face = read_topo.face(face_id).unwrap();
1116 assert!(matches!(face.surface(), FaceSurface::Plane { .. }));
1117 }
1118 }
1119
1120 #[test]
1121 fn empty_input_error() {
1122 let mut topo = Topology::new();
1123 let result = read_step("", &mut topo);
1124 assert!(result.is_err());
1125 }
1126
1127 #[test]
1128 fn no_data_section_error() {
1129 let mut topo = Topology::new();
1130 let result = read_step("ISO-10303-21;\nHEADER;\nENDSEC;\n", &mut topo);
1131 assert!(result.is_err());
1132 }
1133
1134 #[test]
1135 fn parse_refs_basic() {
1136 let refs = parse_refs("'', #10, #20, #30");
1137 assert_eq!(refs, vec![10, 20, 30]);
1138 }
1139
1140 #[test]
1141 fn parse_list_refs_basic() {
1142 let refs = parse_list_refs("'name', (#1, #2, #3), #4");
1143 assert_eq!(refs, vec![1, 2, 3]);
1144 }
1145
1146 #[test]
1147 fn parse_floats_basic() {
1148 let floats = parse_floats("'', (1.5, -2.3, 0.)");
1149 assert_eq!(floats.len(), 3);
1150 assert!((floats[0] - 1.5).abs() < 1e-10);
1151 assert!((floats[1] - (-2.3)).abs() < 1e-10);
1152 assert!((floats[2]).abs() < 1e-10);
1153 }
1154
1155 #[test]
1156 fn parse_floats_scientific() {
1157 let floats = parse_floats("'', (1.000000000000000E+00, -5.000000000000000E-01, 0.)");
1158 assert_eq!(floats.len(), 3);
1159 assert!((floats[0] - 1.0).abs() < 1e-10);
1160 assert!((floats[1] - (-0.5)).abs() < 1e-10);
1161 }
1162
1163 #[test]
1164 fn roundtrip_cylinder_preserves_surface() {
1165 let mut write_topo = Topology::new();
1166 let solid =
1167 brepkit_operations::primitives::make_cylinder(&mut write_topo, 1.5, 3.0).unwrap();
1168
1169 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1170
1171 assert!(step_str.contains("CYLINDRICAL_SURFACE"));
1172
1173 let mut read_topo = Topology::new();
1174 let solids = read_step(&step_str, &mut read_topo).unwrap();
1175 assert!(!solids.is_empty(), "should import at least one solid");
1176
1177 let read_solid = read_topo.solid(solids[0]).unwrap();
1178 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1179
1180 let has_cylinder = shell.faces().iter().any(|&fid| {
1181 matches!(
1182 read_topo.face(fid).unwrap().surface(),
1183 FaceSurface::Cylinder(_)
1184 )
1185 });
1186 assert!(
1187 has_cylinder,
1188 "imported cylinder should have a cylindrical face"
1189 );
1190 }
1191
1192 #[test]
1193 fn roundtrip_nurbs_surface_loft() {
1194 let mut write_topo = Topology::new();
1196
1197 let mut profiles = Vec::new();
1198 for &z in &[0.0, 1.0, 2.0] {
1199 let pts = vec![
1200 Point3::new(-1.0, -1.0, z),
1201 Point3::new(1.0, -1.0, z),
1202 Point3::new(1.0, 1.0, z),
1203 Point3::new(-1.0, 1.0, z),
1204 ];
1205 let wire_id =
1206 brepkit_topology::builder::make_polygon_wire(&mut write_topo, &pts, 1e-7).unwrap();
1207 let v01 = Vec3::new(
1208 pts[1].x() - pts[0].x(),
1209 pts[1].y() - pts[0].y(),
1210 pts[1].z() - pts[0].z(),
1211 );
1212 let v02 = Vec3::new(
1213 pts[2].x() - pts[0].x(),
1214 pts[2].y() - pts[0].y(),
1215 pts[2].z() - pts[0].z(),
1216 );
1217 let normal = v01.cross(v02).normalize().unwrap();
1218 let d = normal.x() * pts[0].x() + normal.y() * pts[0].y() + normal.z() * pts[0].z();
1219 let face = Face::new(wire_id, Vec::new(), FaceSurface::Plane { normal, d });
1220 profiles.push(write_topo.add_face(face));
1221 }
1222 let solid = brepkit_operations::loft::loft_smooth(&mut write_topo, &profiles).unwrap();
1223
1224 let orig_solid = write_topo.solid(solid).unwrap();
1225 let orig_shell = write_topo.shell(orig_solid.outer_shell()).unwrap();
1226 let orig_nurbs_count = orig_shell
1227 .faces()
1228 .iter()
1229 .filter(|&&fid| {
1230 matches!(
1231 write_topo.face(fid).unwrap().surface(),
1232 FaceSurface::Nurbs(_)
1233 )
1234 })
1235 .count();
1236 assert!(orig_nurbs_count > 0, "lofted solid should have NURBS faces");
1237
1238 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1239 assert!(
1240 step_str.contains("B_SPLINE_SURFACE_WITH_KNOTS"),
1241 "STEP output should contain B_SPLINE_SURFACE_WITH_KNOTS"
1242 );
1243
1244 let mut read_topo = Topology::new();
1245 let solids = read_step(&step_str, &mut read_topo).unwrap();
1246 assert!(!solids.is_empty(), "should import at least one solid");
1247
1248 let read_solid = read_topo.solid(solids[0]).unwrap();
1249 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1250
1251 let nurbs_count = shell
1252 .faces()
1253 .iter()
1254 .filter(|&&fid| {
1255 matches!(
1256 read_topo.face(fid).unwrap().surface(),
1257 FaceSurface::Nurbs(_)
1258 )
1259 })
1260 .count();
1261 assert!(
1262 nurbs_count > 0,
1263 "imported solid should have NURBS faces (got {nurbs_count})"
1264 );
1265 assert_eq!(
1266 nurbs_count, orig_nurbs_count,
1267 "NURBS face count should be preserved: {orig_nurbs_count} → {nurbs_count}"
1268 );
1269 }
1270
1271 #[test]
1272 fn roundtrip_nurbs_curve_preserved() {
1273 let mut write_topo = Topology::new();
1275
1276 let mut profiles = Vec::new();
1277 for &z in &[0.0, 1.0, 2.0] {
1278 let pts = vec![
1279 Point3::new(-1.0, -1.0, z),
1280 Point3::new(1.0, -1.0, z),
1281 Point3::new(1.0, 1.0, z),
1282 Point3::new(-1.0, 1.0, z),
1283 ];
1284 let wire_id =
1285 brepkit_topology::builder::make_polygon_wire(&mut write_topo, &pts, 1e-7).unwrap();
1286 let v01 = Vec3::new(
1287 pts[1].x() - pts[0].x(),
1288 pts[1].y() - pts[0].y(),
1289 pts[1].z() - pts[0].z(),
1290 );
1291 let v02 = Vec3::new(
1292 pts[2].x() - pts[0].x(),
1293 pts[2].y() - pts[0].y(),
1294 pts[2].z() - pts[0].z(),
1295 );
1296 let normal = v01.cross(v02).normalize().unwrap();
1297 let d = normal.x() * pts[0].x() + normal.y() * pts[0].y() + normal.z() * pts[0].z();
1298 let face = Face::new(wire_id, Vec::new(), FaceSurface::Plane { normal, d });
1299 profiles.push(write_topo.add_face(face));
1300 }
1301 let solid = brepkit_operations::loft::loft_smooth(&mut write_topo, &profiles).unwrap();
1302
1303 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1304
1305 let has_bspline_curve = step_str.contains("B_SPLINE_CURVE_WITH_KNOTS");
1306
1307 if has_bspline_curve {
1308 let mut read_topo = Topology::new();
1309 let solids = read_step(&step_str, &mut read_topo).unwrap();
1310 assert!(!solids.is_empty());
1311
1312 let read_solid = read_topo.solid(solids[0]).unwrap();
1313 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1314
1315 let has_nurbs_curve = shell.faces().iter().any(|&fid| {
1316 let face = read_topo.face(fid).unwrap();
1317 let wire = read_topo.wire(face.outer_wire()).unwrap();
1318 wire.edges().iter().any(|he| {
1319 matches!(
1320 read_topo.edge(he.edge()).unwrap().curve(),
1321 EdgeCurve::NurbsCurve(_)
1322 )
1323 })
1324 });
1325 assert!(
1326 has_nurbs_curve,
1327 "imported solid should have NURBS edge curves"
1328 );
1329 }
1330 }
1333
1334 #[test]
1335 fn roundtrip_circle_edge_preserved() {
1336 let mut write_topo = Topology::new();
1338 let solid =
1339 brepkit_operations::primitives::make_cylinder(&mut write_topo, 1.0, 2.0).unwrap();
1340
1341 let step_str = writer::write_step(&write_topo, &[solid]).unwrap();
1342 assert!(step_str.contains("CIRCLE"));
1343
1344 let mut read_topo = Topology::new();
1345 let solids = read_step(&step_str, &mut read_topo).unwrap();
1346 assert!(!solids.is_empty());
1347
1348 let read_solid = read_topo.solid(solids[0]).unwrap();
1349 let shell = read_topo.shell(read_solid.outer_shell()).unwrap();
1350
1351 let has_circle = shell.faces().iter().any(|&fid| {
1352 let face = read_topo.face(fid).unwrap();
1353 let wire = read_topo.wire(face.outer_wire()).unwrap();
1354 wire.edges().iter().any(|he| {
1355 matches!(
1356 read_topo.edge(he.edge()).unwrap().curve(),
1357 EdgeCurve::Circle(_)
1358 )
1359 })
1360 });
1361 assert!(
1362 has_circle,
1363 "imported cylinder should have circle edge curves"
1364 );
1365 }
1366
1367 #[test]
1368 fn parse_bspline_surface_attrs_basic() {
1369 let attrs = "'', 1, 1, ((#10, #11), (#12, #13)), .UNSPECIFIED., .F., .F., .F., \
1371 (2, 2), (2, 2), (0.0, 1.0), (0.0, 1.0), .UNSPECIFIED.";
1372 let result = parse_bspline_surface_attrs(attrs);
1373 assert!(result.is_some(), "should parse B_SPLINE_SURFACE attributes");
1374 let (deg_u, deg_v, cp_grid, u_mults, v_mults, u_knots, v_knots) = result.unwrap();
1375 assert_eq!(deg_u, 1);
1376 assert_eq!(deg_v, 1);
1377 assert_eq!(cp_grid.len(), 2);
1378 assert_eq!(cp_grid[0].len(), 2);
1379 assert_eq!(u_mults, vec![2, 2]);
1380 assert_eq!(v_mults, vec![2, 2]);
1381 assert_eq!(u_knots, vec![0.0, 1.0]);
1382 assert_eq!(v_knots, vec![0.0, 1.0]);
1383 }
1384
1385 #[test]
1386 fn parse_bspline_curve_attrs_basic() {
1387 let attrs = "'', 3, (#1, #2, #3, #4), .UNSPECIFIED., .F., .F., \
1388 (4, 4), (0.0, 1.0), .UNSPECIFIED.";
1389 let result = parse_bspline_curve_attrs(attrs);
1390 assert!(result.is_some(), "should parse B_SPLINE_CURVE attributes");
1391 let (degree, cp_refs, mults, knots) = result.unwrap();
1392 assert_eq!(degree, 3);
1393 assert_eq!(cp_refs.len(), 4);
1394 assert_eq!(mults, vec![4, 4]);
1395 assert_eq!(knots, vec![0.0, 1.0]);
1396 }
1397
1398 #[test]
1399 fn expand_knots_basic() {
1400 let mults = [3, 1, 3];
1401 let vals = [0.0, 0.5, 1.0];
1402 let flat = expand_knots(&mults, &vals);
1403 assert_eq!(flat, vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0]);
1404 }
1405
1406 #[test]
1407 fn parse_weight_list_nested() {
1408 let weights = parse_weight_list("(1.0, 0.707, 1.0))");
1410 assert_eq!(weights.len(), 3);
1411 assert!((weights[0] - 1.0).abs() < 1e-10);
1412 assert!((weights[1] - 0.707).abs() < 1e-10);
1413 assert!((weights[2] - 1.0).abs() < 1e-10);
1414 }
1415
1416 #[test]
1417 fn parse_weight_list_flat() {
1418 let weights = parse_weight_list("1.0, 0.707, 1.0)");
1420 assert_eq!(weights.len(), 3);
1421 assert!((weights[0] - 1.0).abs() < 1e-10);
1422 assert!((weights[1] - 0.707).abs() < 1e-10);
1423 assert!((weights[2] - 1.0).abs() < 1e-10);
1424 }
1425
1426 #[test]
1427 fn parse_weight_list_scientific() {
1428 let weights = parse_weight_list("(1.000000E+00, 7.071068E-01))");
1430 assert_eq!(weights.len(), 2);
1431 assert!((weights[0] - 1.0).abs() < 1e-5);
1432 assert!((weights[1] - std::f64::consts::FRAC_1_SQRT_2).abs() < 1e-5);
1433 }
1434
1435 #[test]
1436 fn parse_weight_list_2d_nested() {
1437 let weights = parse_weight_list("((1.0, 0.5), (0.5, 1.0)))");
1439 assert_eq!(weights.len(), 4);
1440 assert!((weights[0] - 1.0).abs() < 1e-10);
1441 assert!((weights[1] - 0.5).abs() < 1e-10);
1442 assert!((weights[2] - 0.5).abs() < 1e-10);
1443 assert!((weights[3] - 1.0).abs() < 1e-10);
1444 }
1445
1446 #[test]
1447 fn extract_rational_weights_from_composite() {
1448 let attrs = "BOUNDED_CURVE() B_SPLINE_CURVE(2, (#1, #2, #3)) \
1449 B_SPLINE_CURVE_WITH_KNOTS((3,3), (0.0, 1.0)) \
1450 RATIONAL_B_SPLINE_CURVE((1.0, 0.707, 1.0))";
1451 let weights = extract_rational_weights(attrs, 3);
1452 assert_eq!(weights.len(), 3);
1453 assert!((weights[1] - 0.707).abs() < 1e-10);
1454 }
1455}