mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-17 22:11:36 +00:00
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:
@@ -33,6 +33,37 @@ bool has_intersection(const std::set<T, Cmp>& A,
|
|||||||
return false;
|
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)
|
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);
|
(relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0. || rotation_a);
|
||||||
|
|
||||||
boost::optional<Eigen::Matrix3d> interpolated_rotation;
|
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) {
|
if (should_interpolate) {
|
||||||
taxonomy::geom_item::ptr profile_b;
|
taxonomy::geom_item::ptr profile_b;
|
||||||
@@ -121,6 +156,41 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
|||||||
|
|
||||||
if (should_interpolate2) {
|
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;
|
std::vector<taxonomy::loop::ptr> loops_a, loops_b;
|
||||||
|
|
||||||
if (profile_a->kind() == taxonomy::FACE) {
|
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);
|
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.
|
// @todo should this not be initialized by default? matrix4 already has a 'lazy identity' mechanism.
|
||||||
loft->children.back()->matrix = taxonomy::make<taxonomy::matrix4>();
|
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();
|
auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval();
|
||||||
loft->children.back()->matrix->components() = m;
|
loft->children.back()->matrix->components() = m;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user