1use axiolid_core::Vec3;
28use axiolid_curve::KnotSpec;
29use axiolid_model::{GeometryNode, NodeId, SurfaceRelation};
30use axiolid_surface::{
31 BSplineSurface as KernelBSpline, Cylinder, Plane as KernelPlane, Sphere, Surface, Torus,
32};
33use ifc_model::EntityId;
34
35use crate::curve::bspline::KnotType;
36use crate::error::GeometryResult;
37use crate::lower::curve::{finite_values, lower_curve_node};
38use crate::lower::session::LoweringSession;
39use crate::resource::direction::resolve_unit;
40use crate::resource::placement::axis_placement_transform;
41use crate::resource::placement::Axis1Placement;
42use crate::resource::point::CartesianPoint;
43use crate::surface::bounded::{CurveBoundedPlane, CurveBoundedSurface, RectangularTrimmedSurface};
44use crate::surface::bspline::BSplineSurface;
45use crate::surface::elementary::{CylindricalSurface, Plane, SphericalSurface, ToroidalSurface};
46use crate::surface::swept::SurfaceOfLinearExtrusion;
47use crate::surface::swept::SurfaceOfRevolution;
48use crate::transform::Transform;
49
50const SURFACE: &str = "surface";
52
53pub fn lower_surface_node(
59 session: &mut LoweringSession<'_>,
60 id: EntityId,
61 frame: Transform,
62) -> GeometryResult<NodeId> {
63 if let Some(existing) = session.memoized(id, SURFACE, frame) {
64 return Ok(existing);
65 }
66 session.enter(id, SURFACE)?;
67 let result = (|| -> GeometryResult<NodeId> {
68 let type_name = session.type_name(id)?.to_ascii_uppercase();
69 match type_name.as_str() {
70 "IFCPLANE" => lower_plane(session, id, frame),
71 "IFCSURFACEOFLINEAREXTRUSION" => lower_linear_extrusion(session, id, frame),
72 "IFCCYLINDRICALSURFACE" => lower_cylinder(session, id, frame),
73 "IFCSPHERICALSURFACE" => lower_sphere(session, id, frame),
74 "IFCTOROIDALSURFACE" => lower_torus(session, id, frame),
75 "IFCBSPLINESURFACEWITHKNOTS" | "IFCRATIONALBSPLINESURFACEWITHKNOTS" => {
76 lower_bspline(session, id, frame)
77 }
78 "IFCSURFACEOFREVOLUTION" => lower_revolution(session, id, frame),
79 "IFCRECTANGULARTRIMMEDSURFACE" => lower_rectangular_trimmed(session, id, frame),
80 "IFCCURVEBOUNDEDPLANE" => lower_curve_bounded(session, id, frame),
81 "IFCCURVEBOUNDEDSURFACE" => lower_curve_bounded_surface(session, id, frame),
82 "IFCSECTIONEDSURFACE" => crate::lower::sectioned::sectioned_surface(session, id, frame),
83 other => Err(session.unsupported(id, other, "curved and B-spline surfaces")),
84 }
85 })();
86 session.exit(id);
87 let node = result?;
88 session.memoize(id, SURFACE, frame, node);
89 Ok(node)
90}
91
92pub fn lower_plane(
98 session: &mut LoweringSession<'_>,
99 id: EntityId,
100 frame: Transform,
101) -> GeometryResult<NodeId> {
102 let entity = session.entity(id, id)?;
103 let view = Plane::new(id, entity);
104 let position_id = view.position_ref()?;
105 let position = session.entity(id, position_id)?;
106 let local = axis_placement_transform(session.model(), position_id, position)?
107 .to_metres(session.units());
108 let placed = frame.compose(&local);
109
110 let normal = placed.apply_direction([0.0, 0.0, 1.0]);
111 let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
112 if !length.is_finite() || length <= f64::EPSILON {
113 return Err(session.degenerate(
114 id,
115 "IFCPLANE",
116 "plane normal is zero-length or non-finite",
117 ));
118 }
119
120 let plane = KernelPlane {
121 frame: placed.to_geom_frame(id)?,
122 };
123 session.node_for(id, GeometryNode::Surface(Surface::Plane(plane)))
124}
125
126pub fn lower_linear_extrusion(
131 session: &mut LoweringSession<'_>,
132 id: EntityId,
133 frame: Transform,
134) -> GeometryResult<NodeId> {
135 let entity = session.entity(id, id)?;
136 let view = SurfaceOfLinearExtrusion::new(id, entity);
137
138 let placed = match view.position_ref() {
140 Some(position_id) => {
141 let position = session.entity(id, position_id)?;
142 let local = axis_placement_transform(session.model(), position_id, position)?
143 .to_metres(session.units());
144 frame.compose(&local)
145 }
146 None => frame,
147 };
148
149 let curve_id = view.swept_curve_ref()?;
150 let swept_curve = swept_generatrix(session, id, curve_id, placed)?;
151
152 let raw = resolve_unit(session.model(), id, view.extruded_direction_ref()?)?;
158 let direction = placed.apply_direction(raw);
159 let length =
160 (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2])
161 .sqrt();
162 if !length.is_finite() || length <= f64::EPSILON {
163 return Err(session.degenerate(
164 id,
165 "IFCSURFACEOFLINEAREXTRUSION",
166 "extruded direction is zero-length or non-finite",
167 ));
168 }
169
170 session.node_for(
171 id,
172 GeometryNode::SurfaceRelation(SurfaceRelation::LinearExtrusion {
173 swept_curve,
174 direction: Vec3::from_array(direction),
175 }),
176 )
177}
178
179#[cfg(test)]
180mod tests;
181
182fn placed_frame(
189 session: &mut LoweringSession<'_>,
190 owner: EntityId,
191 position_id: EntityId,
192 frame: Transform,
193) -> GeometryResult<axiolid_core::Frame3> {
194 let position = session.entity(owner, position_id)?;
195 let local = axis_placement_transform(session.model(), position_id, position)?
196 .to_metres(session.units());
197 frame.compose(&local).to_geom_frame(owner)
198}
199
200fn to_metres(session: &LoweringSession<'_>, value: f64) -> f64 {
202 value * session.units().length_to_metres
203}
204
205pub(crate) fn trim_converter(
211 session: &LoweringSession<'_>,
212 basis_kind: &str,
213) -> impl Fn(f64) -> f64 {
214 let angular = matches!(
215 basis_kind,
216 "IFCCYLINDRICALSURFACE"
217 | "IFCSPHERICALSURFACE"
218 | "IFCTOROIDALSURFACE"
219 | "IFCSURFACEOFREVOLUTION"
220 );
221 let factor = if angular {
222 session.units().angle_to_radians
223 } else {
224 session.units().length_to_metres
225 };
226 move |value: f64| value * factor
227}
228
229fn cartesian_point_3d(
231 session: &LoweringSession<'_>,
232 referrer: EntityId,
233 id: EntityId,
234) -> GeometryResult<[f64; 3]> {
235 let entity = session.entity(referrer, id)?;
236 let point = CartesianPoint::new(id, entity);
237 point.coordinates_3d()
238}
239
240pub fn lower_cylinder(
246 session: &mut LoweringSession<'_>,
247 id: EntityId,
248 frame: Transform,
249) -> GeometryResult<NodeId> {
250 let entity = session.entity(id, id)?;
251 let view = CylindricalSurface::new(id, entity);
252 let placed = placed_frame(session, id, view.position_ref()?, frame)?;
253 let radius = to_metres(session, view.radius()?);
254 session.node_for(
255 id,
256 GeometryNode::Surface(Surface::Cylinder(Cylinder {
257 frame: placed,
258 radius,
259 })),
260 )
261}
262
263pub fn lower_sphere(
265 session: &mut LoweringSession<'_>,
266 id: EntityId,
267 frame: Transform,
268) -> GeometryResult<NodeId> {
269 let entity = session.entity(id, id)?;
270 let view = SphericalSurface::new(id, entity);
271 let placed = placed_frame(session, id, view.position_ref()?, frame)?;
272 let radius = to_metres(session, view.radius()?);
273 session.node_for(
274 id,
275 GeometryNode::Surface(Surface::Sphere(Sphere {
276 frame: placed,
277 radius,
278 })),
279 )
280}
281
282pub fn lower_torus(
288 session: &mut LoweringSession<'_>,
289 id: EntityId,
290 frame: Transform,
291) -> GeometryResult<NodeId> {
292 let entity = session.entity(id, id)?;
293 let view = ToroidalSurface::new(id, entity);
294 let placed = placed_frame(session, id, view.position_ref()?, frame)?;
295 let major_radius = to_metres(session, view.major_radius()?);
296 let minor_radius = to_metres(session, view.minor_radius()?);
297 session.node_for(
298 id,
299 GeometryNode::Surface(Surface::Torus(Torus {
300 frame: placed,
301 major_radius,
302 minor_radius,
303 })),
304 )
305}
306
307fn knot_spec(source: KnotType) -> KnotSpec {
311 match source {
312 KnotType::Uniform => KnotSpec::Uniform,
313 KnotType::QuasiUniform => KnotSpec::QuasiUniform,
314 KnotType::PiecewiseBezier => KnotSpec::PiecewiseBezier,
315 KnotType::Unspecified => KnotSpec::Unspecified,
316 }
317}
318
319pub fn lower_bspline(
331 session: &mut LoweringSession<'_>,
332 id: EntityId,
333 frame: Transform,
334) -> GeometryResult<NodeId> {
335 let entity = session.entity(id, id)?;
336 let view = BSplineSurface::new(id, entity);
337 let type_name = session.type_name(id)?;
338 let u_degree = u16::try_from(view.u_degree()?)
339 .map_err(|_| session.degenerate(id, &type_name, "UDegree exceeds u16"))?;
340 let v_degree = u16::try_from(view.v_degree()?)
341 .map_err(|_| session.degenerate(id, &type_name, "VDegree exceeds u16"))?;
342
343 let u = view.u_knots()?.ok_or_else(|| {
348 session.unsupported(id, &type_name, "B-spline surface without explicit u knots")
349 })?;
350 let v = view.v_knots()?.ok_or_else(|| {
351 session.unsupported(id, &type_name, "B-spline surface without explicit v knots")
352 })?;
353 let u_declared: u128 = u.multiplicities.iter().map(|m| *m as u128).sum();
354 session.check_aggregate(id, &type_name, "u knot multiplicities", u_declared)?;
355 let v_declared: u128 = v.multiplicities.iter().map(|m| *m as u128).sum();
356 session.check_aggregate(id, &type_name, "v knot multiplicities", v_declared)?;
357
358 let grid = view.control_points()?;
359 session.check_aggregate(
362 id,
363 &type_name,
364 "control grid points",
365 grid.u_count() as u128 * grid.v_count() as u128,
366 )?;
367 let mut control_points = Vec::with_capacity(grid.u_count());
368 for row in grid.rows() {
369 let mut out_row = Vec::with_capacity(row.len());
370 for point_id in row {
371 let raw = cartesian_point_3d(session, id, *point_id)?;
372 let placed = frame.apply(raw.map(|value| to_metres(session, value)));
373 finite_values(
374 session,
375 id,
376 &type_name,
377 "transformed control point",
378 &placed,
379 )?;
380 out_row.push(axiolid_core::Point3::from_array(placed));
381 }
382 control_points.push(out_row);
383 }
384
385 finite_values(session, id, &type_name, "UKnots", &u.values)?;
386 finite_values(session, id, &type_name, "VKnots", &v.values)?;
387 let u_multiplicities = multiplicities(session, id, &type_name, u.multiplicities)?;
388 let v_multiplicities = multiplicities(session, id, &type_name, v.multiplicities)?;
389 let weights = view.weights()?;
390 if let Some(rows) = weights.as_ref() {
391 for row in rows {
392 finite_values(session, id, &type_name, "WeightsData", row)?;
393 }
394 }
395 let u_closed = view.u_closed().ok_or_else(|| {
396 session.unsupported(id, &type_name, "unknown UClosed is not lossless in bool")
397 })?;
398 let v_closed = view.v_closed().ok_or_else(|| {
399 session.unsupported(id, &type_name, "unknown VClosed is not lossless in bool")
400 })?;
401
402 let surface = KernelBSpline {
403 u_degree,
404 v_degree,
405 control_points,
406 u_knots: u.values,
407 u_multiplicities,
408 v_knots: v.values,
409 v_multiplicities,
410 weights,
411 u_closed,
412 v_closed,
413 self_intersect: view.self_intersect(),
414 knot_spec: knot_spec(view.knot_spec()),
415 };
416 session.node_for(id, GeometryNode::Surface(Surface::BSpline(surface)))
417}
418
419fn multiplicities(
420 session: &LoweringSession<'_>,
421 id: EntityId,
422 type_name: &str,
423 values: Vec<usize>,
424) -> GeometryResult<Vec<u32>> {
425 values
426 .into_iter()
427 .map(|value| {
428 u32::try_from(value)
429 .map_err(|_| session.degenerate(id, type_name, "multiplicity exceeds u32"))
430 })
431 .collect()
432}
433
434fn swept_generatrix(
442 session: &mut LoweringSession<'_>,
443 _owner: EntityId,
444 swept_curve: EntityId,
445 frame: Transform,
446) -> GeometryResult<NodeId> {
447 let kind = session.type_name(swept_curve)?.to_ascii_uppercase();
448 let curve_id = if kind == "IFCARBITRARYOPENPROFILEDEF" || kind == "IFCARBITRARYCLOSEDPROFILEDEF"
449 {
450 session.slots(swept_curve)?.req_ref(2, "Curve")?
453 } else {
454 swept_curve
455 };
456 lower_curve_node(session, curve_id, frame)
457}
458
459pub fn lower_revolution(
467 session: &mut LoweringSession<'_>,
468 id: EntityId,
469 frame: Transform,
470) -> GeometryResult<NodeId> {
471 let entity = session.entity(id, id)?;
472 let view = SurfaceOfRevolution::new(id, entity);
473
474 let swept_curve = swept_generatrix(session, id, view.swept_curve_ref()?, frame)?;
475
476 let axis_id = view.axis_position_ref()?;
477 let axis_entity = session.entity(id, axis_id)?;
478 let axis = Axis1Placement::new(axis_id, axis_entity);
479 let raw_origin = axis.location(session.model())?;
480 let origin = frame.apply([
481 to_metres(session, raw_origin[0]),
482 to_metres(session, raw_origin[1]),
483 to_metres(session, raw_origin[2]),
484 ]);
485 let direction = frame.apply_direction(axis.axis(session.model())?);
486
487 session.node_for(
488 id,
489 GeometryNode::SurfaceRelation(SurfaceRelation::Revolution {
490 swept_curve,
491 axis_origin: axiolid_core::Point3::from_array(origin),
492 axis_direction: axiolid_core::Vec3::from_array(direction),
493 }),
494 )
495}
496
497pub fn lower_rectangular_trimmed(
504 session: &mut LoweringSession<'_>,
505 id: EntityId,
506 frame: Transform,
507) -> GeometryResult<NodeId> {
508 let entity = session.entity(id, id)?;
509 let view = RectangularTrimmedSurface::new(id, entity);
510 let basis_id = view.basis_surface_ref()?;
511 let basis = lower_surface_node(session, basis_id, frame)?;
512 let rect = view.rectangle()?;
513
514 let basis_kind = session.type_name(basis_id)?.to_ascii_uppercase();
515 let convert = trim_converter(session, &basis_kind);
516
517 session.node_for(
518 id,
519 GeometryNode::SurfaceRelation(SurfaceRelation::RectangularTrimmed {
520 basis,
521 u: (convert(rect.u1), convert(rect.u2)),
522 v: (convert(rect.v1), convert(rect.v2)),
523 u_sense: rect.usense,
524 v_sense: rect.vsense,
525 }),
526 )
527}
528
529pub fn lower_curve_bounded(
542 session: &mut LoweringSession<'_>,
543 id: EntityId,
544 frame: Transform,
545) -> GeometryResult<NodeId> {
546 let entity = session.entity(id, id)?;
547 let view = CurveBoundedPlane::new(id, entity);
548 let basis = lower_surface_node(session, view.basis_surface_ref()?, frame)?;
549
550 let parameter_space = Transform::identity();
551 let mut boundaries = vec![lower_curve_node(
552 session,
553 view.outer_boundary_ref()?,
554 parameter_space,
555 )?];
556 for inner in view.inner_boundary_refs() {
557 boundaries.push(lower_curve_node(session, inner, parameter_space)?);
558 }
559
560 session.node_for(
561 id,
562 GeometryNode::SurfaceRelation(SurfaceRelation::CurveBounded {
563 basis,
564 boundaries,
565 implicit_outer: false,
566 }),
567 )
568}
569
570pub fn lower_curve_bounded_surface(
573 session: &mut LoweringSession<'_>,
574 id: EntityId,
575 frame: Transform,
576) -> GeometryResult<NodeId> {
577 let entity = session.entity(id, id)?;
578 let view = CurveBoundedSurface::new(id, entity);
579 let basis = lower_surface_node(session, view.basis_surface_ref()?, frame)?;
580 let refs = view.boundary_refs()?;
581 let mut boundaries = Vec::with_capacity(refs.len());
582 for boundary in refs {
583 boundaries.push(lower_curve_node(session, boundary, frame)?);
584 }
585
586 session.node_for(
587 id,
588 GeometryNode::SurfaceRelation(SurfaceRelation::CurveBounded {
589 basis,
590 boundaries,
591 implicit_outer: view.implicit_outer(),
592 }),
593 )
594}