Loft constant curved cross sections instead of crashing (#7658)

make_loft interpolated every intermediate cross section by decomposing each
loop into point3 vertices and lerping them. A curved section (arc, circle,
b-spline) stores its edge trims as curve parameters, not point3, so
boost::get<point3> threw boost::bad_get (GEO327) and any curved-section
IfcSectionedSolidHorizontal failed to convert. The point-lerp path is
polygonal only by construction, and the placement rotation alone forces the
interpolation branch, so essentially all curved input crashed before a
solid was built.

Detect a non-polygonal (curved) section with the non-throwing pointer form
of boost::get. When the section shape is constant along the segment (same
profile instance or equal hash), pass the profile through to the existing
non-polygonal ThruSections path in loft.cpp with the per-station placement,
rotation, and a baked lateral offset carrying the variation, instead of
vertex-decomposing it. A genuine morph between two different curved sections
cannot be expressed this way and is now reported (GEO 50) and skipped
gracefully rather than crashing. Polygonal sections keep the original
point-interpolation path unchanged.

Verified (IFC4X3_ADD2, OCC 7.9.2, create_shape + iterator): a circular
constant-section loft goes from a GEO327 bad_get crash (0 verts) to a closed
manifold solid (10100 verts, 0 boundary / 0 non-manifold edges); a polygonal
rectangle control is unchanged (404 verts, closed); a genuine circle r=1 to
r=2 morph now logs GEO050 and skips instead of crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Petru Conduraru
2026-07-11 11:03:50 +03:00
parent 256d5a63f1
commit a1d1a2a615
+80
View File
@@ -33,6 +33,37 @@ bool has_intersection(const std::set<T, Cmp>& A,
return false;
}
// A loop is polygonal when every edge is a straight line segment whose
// endpoints are stored as explicit points. Curved edges (arcs, circles,
// b-splines) carry a non-line basis and may store their trims as curve
// parameters rather than point3 vertices, so they cannot be decomposed into
// a vertex list without dropping their curvature (and boost::get<point3>
// would throw boost::bad_get on a parameter-valued trim).
bool loop_is_polygonal(const taxonomy::loop::ptr& loop) {
for (const auto& e : loop->children) {
if (e->basis != nullptr && e->basis->kind() != taxonomy::LINE) {
return false;
}
if (boost::get<taxonomy::point3::ptr>(&e->start) == nullptr ||
boost::get<taxonomy::point3::ptr>(&e->end) == nullptr) {
return false;
}
}
return true;
}
bool profile_is_polygonal(const taxonomy::geom_item::ptr& profile) {
if (profile->kind() == taxonomy::FACE) {
for (const auto& child : std::static_pointer_cast<taxonomy::face>(profile)->children) {
if (!loop_is_polygonal(child)) {
return false;
}
}
return true;
}
return loop_is_polygonal(std::static_pointer_cast<taxonomy::loop>(profile));
}
}
taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, const IfcUtil::IfcBaseClass* inst, const taxonomy::function_item::ptr& fn, std::vector<cross_section>& cross_sections, Logger& logger)
@@ -99,6 +130,10 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
(relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0. || rotation_a);
boost::optional<Eigen::Matrix3d> interpolated_rotation;
// Lateral offset to bake into the placement when a curved (non
// polygonal) constant section is passed through instead of being
// vertex-interpolated.
boost::optional<Eigen::Vector3d> passthrough_offset;
if (should_interpolate) {
taxonomy::geom_item::ptr profile_b;
@@ -121,6 +156,41 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
if (should_interpolate2) {
// Vertex-wise interpolation is only meaningful for polygonal
// loops. If either section is curved we cannot decompose it into
// a point list. When the section shape is constant along this
// segment (same profile instance) we pass the profile through so
// it is placed rigidly along the directrix (rotation and offset
// handled below); a genuine morph between two different curved
// sections is not supported yet and is reported rather than
// crashing with boost::bad_get.
const bool curved_sections = !profile_is_polygonal(profile_a) || !profile_is_polygonal(profile_b);
// A constant section (same shape at both ends) only changes
// placement along the directrix, which is applied below; the
// curvature is preserved by passing the profile through. A
// genuine morph between two differently shaped curved sections
// cannot be expressed this way and is reported instead of
// crashing.
const bool same_section_shape =
(profile_a->instance == profile_b->instance) ||
(profile_a->hash() == profile_b->hash());
if (curved_sections && !same_section_shape) {
logger.Error("GEO", 50, "Interpolation between differing curved (non-polygonal) cross sections is not supported", inst);
return nullptr;
}
if (curved_sections) {
// Constant curved section: bake the interpolated lateral
// offset into the placement and let the clone path below emit
// profile_a. 'interpolated' stays null.
passthrough_offset = lerp(offset_a, offset_b, relative_dist_along);
if (rotation_a.has_value() && rotation_b.has_value()) {
interpolated_rotation = lerp(*rotation_a, *rotation_b, relative_dist_along);
} else if (rotation_a != rotation_b) {
logger.Error("GEO", 42, "Direction vectors on cross section placements only supported when used consistently");
}
} else {
std::vector<taxonomy::loop::ptr> loops_a, loops_b;
if (profile_a->kind() == taxonomy::FACE) {
@@ -392,6 +462,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
}
}
}
}
}
auto m4 = evaluator.evaluate(dist_along);
@@ -428,6 +499,15 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
// @todo should this not be initialized by default? matrix4 already has a 'lazy identity' mechanism.
loft->children.back()->matrix = taxonomy::make<taxonomy::matrix4>();
}
if (passthrough_offset && passthrough_offset->cwiseAbs().maxCoeff() > 0.) {
// Constant curved section that was passed through instead of
// vertex-interpolated: apply the lateral offset in the profile's
// local frame (matching where the offset is added on the
// polygonal point-interpolation path).
Eigen::Matrix4d t = Eigen::Matrix4d::Identity();
t.col(3).head<3>() = *passthrough_offset;
loft->children.back()->matrix->components() = (loft->children.back()->matrix->ccomponents() * t).eval();
}
auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval();
loft->children.back()->matrix->components() = m;
}