Compare commits

...

2 Commits

Author SHA1 Message Date
Petru Conduraru d053530743 ifcgeom: butt-join near-tangent sweep corners instead of hanging #8400
A second, independent trigger for the MakePipeShell hang from #8400: a kink
angle at a directrix joint in roughly the 0.57-5 degree band produces two
nearly-coaxial pipe surfaces at the corner, and BRepFill_TrimShellCorner's
projection (ProjLib_CompProjectedCurve::Init) never converges. This happens
regardless of chord length, so the sub-radius chord collapse already fixed in
the directrix-hang commit does not prevent it.

BRepOffsetAPI_MakePipeShell::SetTransitionMode always leaves Angmin (the kink
angle below which OCCT treats a joint as a safe butt-join rather than routing
it through the corner trim) at its hardcoded default of 1e-2 rad, and does not
expose a way to change it. Confirmed by pulling the OCCT 7.9.2 source: the
wrapper is a 1:1 forwarding layer over BRepFill_PipeShell (same Add/Set/Build/
FirstShape/LastShape/Shape semantics), and its own Build() discards the
Message_ProgressRange argument it declares, so no progress/cancellation signal
reaches this path either way. Switch to BRepFill_PipeShell directly and raise
Angmin to 0.1 rad (~5.7 deg) so the dangerous band takes the butt-join path.

Verified against the reporter's minimal repro (IfcCableCarrierSegment, 101-
segment composite curve directrix, disk radius 1.6cm, kink angles 0.15-2 deg):
clean v0.8.0 and v0.8.0 with only the prior directrix-collapse fix both still
hang (30s+ timeout, matches the reported bug); with this change it converts in
~1s (4781 verts, 5684 faces, no validation errors). Independently bisected the
Angmin threshold on this repro: 0.02 rad still hangs, 0.06 rad fails cleanly
("BRep_API: command not done", no hang), 0.1 rad converges.

Regression-checked all 50 test/input fixtures containing IfcSweptDiskSolid
(the ones parseable under this build's IFC4-only schema config produce
identical exit codes and, where conversion succeeds outright, identical
output, before and after) and a synthetic 30 degree corner (well above the
new threshold): byte-identical output, confirming ordinary corners are
unaffected. One real fixture (987--cableSegment) has joints that fall in the
newly-affected band and now converts with fewer vertices/faces at those
corners (butt-join instead of trim); both variants pass --validate with no
errors, and nearest-neighbour vertex deviation between the two outputs peaks
under 1mm on a 3.9mm-radius cable, i.e. a real but small and bounded fidelity
trade at corners that previously could hang forever.

Generated with the assistance of an AI coding tool.
2026-07-19 07:13:40 +03:00
Petru Conduraru 779f6e0922 ifcgeom: collapse degenerate directrix segments to avoid a MakePipeShell hang #8400
IfcConvert hangs forever on a swept-disk solid whose IfcCompositeCurve
directrix contains segments far shorter than the disk radius. Where a segment
is shorter than OCCT's corner-trim region (which scales with the pipe radius),
BRepFill_TrimShellCorner must trim two nearly-coincident pipe surfaces and
ProjLib_CompProjectedCurve's Newton walk never converges, so
BRepOffsetAPI_MakePipeShell::Build spins indefinitely. That OCCT path checks no
UserBreak/progress range, so it cannot be interrupted from the outside.

Sanitize the directrix before the sweep: for open, purely linear directrix
wires, collapse any segment shorter than max(radius/10, 10*precision) to its
midpoint (each vertex moves below the disk radius, in a region the disk cannot
resolve), pinning the endpoints and leaving non-linear or near-closed wires
untouched. Emit GEO 259 via the kernel logger_ when a collapse occurs.

Verified with a local OpenCascade build (OCC 7.9.2): the reported sample hangs
before (killed after 90 s, ~84 s CPU) and terminates after in ~285 ms with a
valid tube (2956 verts), logging "Collapsed 16 degenerate directrix segment(s)"
(matching the reporter's count). Clean swept solids trigger no collapse and are
unchanged.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:53:25 +03:00
@@ -22,6 +22,7 @@
#include "wire_utils.h"
#include <BRepOffsetAPI_MakePipeShell.hxx>
#include <BRepFill_PipeShell.hxx>
#include <Geom_Plane.hxx>
#include <ShapeAnalysis_Surface.hxx>
#include <BRepBuilderAPI_Transform.hxx>
@@ -30,6 +31,10 @@
#include <TopExp.hxx>
#include <Geom_Circle.hxx>
#include <BRepBuilderAPI_MakeSolid.hxx>
#include <BRepTools_WireExplorer.hxx>
#include <BRepBuilderAPI_MakePolygon.hxx>
#include <Geom_Line.hxx>
#include <Geom_TrimmedCurve.hxx>
using namespace ifcopenshell::geometry;
using namespace ifcopenshell::geometry::kernels;
@@ -81,6 +86,82 @@ namespace {
}
return false;
}
// Collapses ultra-short straight segments in an open, purely linear directrix.
//
// Revit MEP flexible-conduit exports discretize a spline directrix with chords
// far shorter than the swept disk radius (down to ~1/50 of the radius). Where a
// directrix segment is shorter than the corner trim region (which scales with
// the pipe radius), BRepFill_TrimShellCorner has to trim two nearly-coincident
// pipe surfaces; ProjLib_CompProjectedCurve's Newton walk then fails to converge
// and BRepOffsetAPI_MakePipeShell::Build() spins forever. No UserBreak/progress
// range is checked anywhere in that OCCT path, so the hang cannot be interrupted
// from the outside (see IfcOpenShell #8400).
//
// Interior vertices bordering a sub-threshold segment collapse to that segment's
// midpoint (each vertex moves at most min_len/2, i.e. below the disk radius, in a
// region the disk cannot resolve anyway); the directrix endpoints are pinned.
// Wires containing any non-linear segment, or (nearly) closed wires, are left
// untouched, so ordinary directrices are unaffected.
bool collapse_short_directrix_edges(TopoDS_Wire& w, double min_len, int& n_collapsed) {
std::vector<gp_Pnt> pts;
for (BRepTools_WireExplorer exp(w); exp.More(); exp.Next()) {
const TopoDS_Edge& e = exp.Current();
double u0, u1;
Handle(Geom_Curve) crv = BRep_Tool::Curve(e, u0, u1);
if (crv.IsNull()) {
return false;
}
if (crv->DynamicType() == STANDARD_TYPE(Geom_TrimmedCurve)) {
crv = Handle(Geom_TrimmedCurve)::DownCast(crv)->BasisCurve();
}
if (crv->DynamicType() != STANDARD_TYPE(Geom_Line)) {
return false;
}
if (pts.empty()) {
pts.push_back(BRep_Tool::Pnt(TopExp::FirstVertex(e, Standard_True)));
}
pts.push_back(BRep_Tool::Pnt(TopExp::LastVertex(e, Standard_True)));
}
// Need at least two segments, and keep genuinely closed loops out of scope.
if (pts.size() < 3 || pts.front().Distance(pts.back()) < min_len) {
return false;
}
bool changed = true;
while (changed) {
changed = false;
for (size_t i = 0; i + 1 < pts.size() && pts.size() > 2; ++i) {
if (pts[i].Distance(pts[i + 1]) >= min_len) {
continue;
}
if (i == 0) {
// keep the directrix start point fixed
pts.erase(pts.begin() + 1);
} else if (i + 2 == pts.size()) {
// keep the directrix end point fixed
pts.erase(pts.begin() + i);
} else {
pts[i] = gp_Pnt((pts[i].XYZ() + pts[i + 1].XYZ()) / 2.);
pts.erase(pts.begin() + i + 1);
}
++n_collapsed;
changed = true;
break;
}
}
if (n_collapsed == 0) {
return false;
}
BRepBuilderAPI_MakePolygon mp;
for (const auto& p : pts) {
mp.Add(p);
}
if (!mp.IsDone()) {
return false;
}
w = mp.Wire();
return true;
}
}
bool OpenCascadeKernel::convert(const taxonomy::sweep_along_curve::ptr scs, TopoDS_Shape& result) {
@@ -160,6 +241,26 @@ bool OpenCascadeKernel::convert(const taxonomy::sweep_along_curve::ptr scs, Topo
gp_Trsf directrix;
TopoDS_Wire wire = boost::get<TopoDS_Wire>(w);
{
// Sanitize degenerate directrix segments that would make OCCT's
// MakePipeShell corner-trimming loop forever (see #8400). The threshold
// is kept well below the swept disk radius, so only sub-feature-scale
// noise (which the disk cannot resolve) is collapsed; it is floored at a
// small multiple of model precision for the tiny/degenerate radius case.
double radius = 0.;
if (scs->basis && scs->basis->kind() == taxonomy::FACE) {
auto& loops = std::static_pointer_cast<taxonomy::face>(scs->basis)->children;
if (!loops.empty() && loops[0]->children.size() == 1 && loops[0]->children[0]->basis && loops[0]->children[0]->basis->kind() == taxonomy::CIRCLE) {
radius = std::static_pointer_cast<taxonomy::circle>(loops[0]->children[0]->basis)->radius;
}
}
const double min_len = (std::max)(radius / 10., settings_.get<settings::Precision>().get() * 10.);
int n_collapsed = 0;
if (collapse_short_directrix_edges(wire, min_len, n_collapsed)) {
logger_.Message(Logger::LOG_WARNING, "GEO", 259, "Collapsed " + std::to_string(n_collapsed) + " degenerate directrix segment(s) shorter than a tenth of the swept disk radius", scs->instance);
}
}
const bool is_plane = surface && surface->DynamicType() == STANDARD_TYPE(Geom_Plane);
gp_Pln pln;
@@ -255,20 +356,22 @@ bool OpenCascadeKernel::convert(const taxonomy::sweep_along_curve::ptr scs, Topo
continue;
}
BRepOffsetAPI_MakePipeShell builder(wire);
builder.Add(section);
builder.SetTransitionMode(contains_circular_segments(wire) && wire_is_c1_continuous(wire, 1.e-2) ? BRepBuilderAPI_Transformed : BRepBuilderAPI_RightCorner);
// Raise Angmin so near-tangent corners butt-join instead of hanging in
// BRepFill_TrimShellCorner (#8400); not exposed via BRepOffsetAPI_MakePipeShell.
const double transition_angmin = 0.1; // rad, ~5.7 deg
Handle(BRepFill_PipeShell) builder = new BRepFill_PipeShell(wire);
builder->Add(section);
builder->SetTransition(contains_circular_segments(wire) && wire_is_c1_continuous(wire, 1.e-2) ? BRepFill_Modified : BRepFill_Right, transition_angmin);
if (directrix_on_plane) {
builder.SetMode(pln.Axis().Direction());
builder->Set(pln.Axis().Direction());
} else if (!is_plane) {
builder.SetMode(surface_face);
builder->Set(surface_face);
}
builder.Build();
if (!builder.IsDone()) {
if (!builder->Build()) {
return false;
}
auto w0 = TopoDS::Wire(builder.FirstShape());
auto w1 = TopoDS::Wire(builder.LastShape());
auto w0 = TopoDS::Wire(builder->FirstShape());
auto w1 = TopoDS::Wire(builder->LastShape());
if (mf0) {
mf0->Add(w0);
mf1->Add(w1);
@@ -282,7 +385,7 @@ bool OpenCascadeKernel::convert(const taxonomy::sweep_along_curve::ptr scs, Topo
mf1.reset(new BRepBuilderAPI_MakeFace(f1));
}
for (TopExp_Explorer exp2(builder.Shape(), TopAbs_FACE); exp2.More(); exp2.Next()) {
for (TopExp_Explorer exp2(builder->Shape(), TopAbs_FACE); exp2.More(); exp2.Next()) {
BB.Add(comp, exp2.Current());
}
}