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