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