diff --git a/ChangeLog.md b/ChangeLog.md index 1feb201b12..c6ee591643 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,14 @@ Changelog ========= +## [Unreleased] + +### Breaking Changes +- `CTiglAbstractGeometricComponent::GetLoft()` now returns untrimmed loft by default. Use `GetTrimmedLoft()` for the previous behavior (with UV cutting at profile positions). This affects `CCPACSWing` and `CCPACSFuselage` (and their segments). [#1262](https://github.com/DLR-SC/tigl/issues/1262) + +### Features +- Add optional UV profile cutting to `CTiglMakeLoft` via `setEnableProfileCutting(bool)`. When enabled, creates seams at each profile wire position for consistent UV parameterization. Wing and fuselage builders now build both trimmed and untrimmed lofts on demand. [#1262](https://github.com/DLR-SC/tigl/issues/1262) + Changes since last release ---------------- 2025/09/26 diff --git a/TIGLCreator/src/TIGLCreatorContext.cpp b/TIGLCreator/src/TIGLCreatorContext.cpp index 77b953e837..f689b4425b 100644 --- a/TIGLCreator/src/TIGLCreatorContext.cpp +++ b/TIGLCreator/src/TIGLCreatorContext.cpp @@ -396,6 +396,7 @@ Handle(AIS_Shape) TIGLCreatorContext::displayShape(const TopoDS_Shape& loft, boo myContext->SetTransparency(shape, transparency, Standard_False); myContext->SetDisplayMode(shape, shaded, Standard_False); shape->SetOwnDeviationCoefficient(settings.tesselationAccuracy()); + shape->SetOwnDeviationAngle(settings.tesselationDeviationAngle()); #if OCC_VERSION_HEX >= VERSION_HEX_CODE(6,7,0) if (!myShader.IsNull()) { @@ -433,6 +434,7 @@ Handle(AIS_Shape) TIGLCreatorContext::displayShape(const PNamedShape& pshape, bo myContext->SetTransparency(shape, transparency, Standard_False); myContext->SetDisplayMode(shape, shaded, Standard_False); shape->SetOwnDeviationCoefficient(settings.tesselationAccuracy()); + shape->SetOwnDeviationAngle(settings.tesselationDeviationAngle()); #if OCC_VERSION_HEX >= VERSION_HEX_CODE(6,7,0) if (!myShader.IsNull()) { @@ -683,6 +685,7 @@ Handle(AIS_InteractiveObject) TIGLCreatorContext::displayShapeHLMode(const TopoD myContext->SetTransparency(shape, transparency, Standard_False); myContext->SetWidth(shape, 3, Standard_False); shape->SetOwnDeviationCoefficient(settings.tesselationAccuracy()); + shape->SetOwnDeviationAngle(settings.tesselationDeviationAngle()); #if OCC_VERSION_HEX >= VERSION_HEX_CODE(6, 7, 0) if (!myShader.IsNull()) { diff --git a/TIGLCreator/src/TIGLCreatorDocument.cpp b/TIGLCreator/src/TIGLCreatorDocument.cpp index b3867fc839..82a017c320 100644 --- a/TIGLCreator/src/TIGLCreatorDocument.cpp +++ b/TIGLCreator/src/TIGLCreatorDocument.cpp @@ -1752,7 +1752,7 @@ void TIGLCreatorDocument::drawAllFuselagesAndWingsSurfacePoints() for (int fuselageIndex = 1; fuselageIndex <= GetConfiguration().GetFuselageCount(); fuselageIndex++) { auto& fuselage = GetConfiguration().GetFuselage(fuselageIndex); - app->getScene()->displayShape(fuselage.GetLoft(), true, getDefaultShapeColor()); +app->getScene()->displayShape(fuselage.GetLoft(), true, getDefaultShapeColor()); for (int segmentIndex = 1; segmentIndex <= fuselage.GetSegmentCount(); segmentIndex++) { // Draw some points on the fuselage segment diff --git a/TIGLCreator/src/TIGLCreatorSettings.cpp b/TIGLCreator/src/TIGLCreatorSettings.cpp index 4185c60fcb..e257f672a4 100644 --- a/TIGLCreator/src/TIGLCreatorSettings.cpp +++ b/TIGLCreator/src/TIGLCreatorSettings.cpp @@ -21,12 +21,13 @@ #include #include +#include #include "TIGLCreatorMaterials.h" #include #include #include "TIGLCreatorSettings.h" -const double DEFAULT_TESSELATION_ACCURACY = 0.000316; +const double DEFAULT_TESSELATION_ACCURACY = 0.004; const double DEFAULT_TRIANGULATION_ACCURACY = 0.00070; const QColor DEFAULT_BGCOLOR(169,237,255); const QColor DEFAULT_SHAPE_COLOR(0, 170 ,255, 255); @@ -114,6 +115,23 @@ void TIGLCreatorSettings::setDefaultMaterial(const QString& material) _defaultMaterial = tiglMaterials::materialMap[material]; } +double TIGLCreatorSettings::tesselationDeviationAngle() const +{ + // Map current tesselation accuracy (linear) to an angular deviation using the same logarithmic mapping + // NOTE: intentionally decoupled from the dialog slider endpoints. The angle + // is a pure function of the accuracy value, so keeping these fixed means a + // given accuracy always yields the same angle regardless of slider remapping. + const double WORST_TESSELATION = 0.01; + const double BEST_TESSELATION = 0.000002; + const double WORST_ANGLE = 0.15; // rad (~8.6°) coarse + const double BEST_ANGLE = 0.012; // rad (~0.69°) fine + double t = std::log(WORST_TESSELATION / _tesselationAccuracy) / + std::log(WORST_TESSELATION / BEST_TESSELATION); + t = std::max(0.0, std::min(1.0, t)); + // Interpolate angle in log-space + return WORST_ANGLE * std::pow(BEST_ANGLE / WORST_ANGLE, t); +} + double TIGLCreatorSettings::tesselationAccuracy() const { return _tesselationAccuracy; diff --git a/TIGLCreator/src/TIGLCreatorSettings.h b/TIGLCreator/src/TIGLCreatorSettings.h index 682d32c7bd..d77e925179 100644 --- a/TIGLCreator/src/TIGLCreatorSettings.h +++ b/TIGLCreator/src/TIGLCreatorSettings.h @@ -47,6 +47,7 @@ class TIGLCreatorSettings : public QObject void setTriangulationAccuracy(double); double tesselationAccuracy() const; + double tesselationDeviationAngle() const; double triangulationAccuracy() const; void setBGColor(const QColor&); diff --git a/TIGLCreator/src/TIGLCreatorSettingsDialog.cpp b/TIGLCreator/src/TIGLCreatorSettingsDialog.cpp index 98406f66ad..fa470e7c53 100644 --- a/TIGLCreator/src/TIGLCreatorSettingsDialog.cpp +++ b/TIGLCreator/src/TIGLCreatorSettingsDialog.cpp @@ -33,8 +33,8 @@ #include "TIGLCreatorSettingsDialog.h" -#define WORST_TESSELATION 0.01 -#define BEST_TESSELATION 0.00001 +#define WORST_TESSELATION 0.05 +#define BEST_TESSELATION 0.0005 #define WORST_TRIANGULATION 0.01 #define BEST_TRIANGULATION 0.00005 @@ -157,7 +157,8 @@ void TIGLCreatorSettingsDialog::updateEntries() double mu = log(dmax/dmin)/double(imax-imin); double c = dmax / exp(-mu * (double)imin); - int tessVal = int (log(c/_settings.tesselationAccuracy())/mu); + int tessVal = static_cast(std::lround(log(c/_settings.tesselationAccuracy())/mu)); + tessVal = std::clamp(tessVal, sliderTesselationAccuracy->minimum(), sliderTesselationAccuracy->maximum()); sliderTesselationAccuracy->setValue(tessVal); dmax = WORST_TRIANGULATION, dmin = BEST_TRIANGULATION; @@ -166,7 +167,8 @@ void TIGLCreatorSettingsDialog::updateEntries() mu = log(dmax/dmin)/double(imax-imin); c = dmax / exp(-mu * (double)imin); - int triaVal = int (log(c/_settings.triangulationAccuracy())/mu); + int triaVal = static_cast(std::lround(log(c/_settings.triangulationAccuracy())/mu)); + triaVal = std::clamp(triaVal, sliderTriangulationAccuracy->minimum(), sliderTriangulationAccuracy->maximum()); sliderTriangulationAccuracy->setValue(triaVal); _bgcolor = _settings.BGColor(); diff --git a/src/common/tiglcommonfunctions.cpp b/src/common/tiglcommonfunctions.cpp index 4a49ce4005..bf3e75f1ce 100644 --- a/src/common/tiglcommonfunctions.cpp +++ b/src/common/tiglcommonfunctions.cpp @@ -185,6 +185,18 @@ unsigned int GetNumberOfFaces(const TopoDS_Shape& shape) return iFaces; } +int FacesPerSegment(int nFaces, int nSegments) +{ + if (nSegments <= 0) { + return 1; + } + int facesPerSegment = (nFaces + nSegments - 1) / nSegments; + if (facesPerSegment < 1) { + facesPerSegment = 1; + } + return facesPerSegment; +} + unsigned int GetNumberOfSubshapes(const TopoDS_Shape &shape) { if (shape.ShapeType() == TopAbs_COMPOUND) { diff --git a/src/common/tiglcommonfunctions.h b/src/common/tiglcommonfunctions.h index de8f7caf78..ec266883c4 100644 --- a/src/common/tiglcommonfunctions.h +++ b/src/common/tiglcommonfunctions.h @@ -177,6 +177,11 @@ TIGL_EXPORT unsigned int GetNumberOfEdges(const TopoDS_Shape& shape); // returns the number of faces of the current shape TIGL_EXPORT unsigned int GetNumberOfFaces(const TopoDS_Shape& shape); +// Distributes nFaces faces evenly over nSegments segments, rounding up so that +// every face is covered (ceil division). The result is clamped to a minimum of 1. +// Used to determine the number of loft faces per profile segment. +TIGL_EXPORT int FacesPerSegment(int nFaces, int nSegments); + TIGL_EXPORT TopoDS_Edge GetEdge(const TopoDS_Shape& shape, int iEdge); TIGL_EXPORT TopoDS_Face GetFace(const TopoDS_Shape& shape, int iFace); diff --git a/src/control_devices/CCPACSControlSurfaceOuterShapeLeadingEdge.cpp b/src/control_devices/CCPACSControlSurfaceOuterShapeLeadingEdge.cpp index 0538e133ae..ab8866f553 100644 --- a/src/control_devices/CCPACSControlSurfaceOuterShapeLeadingEdge.cpp +++ b/src/control_devices/CCPACSControlSurfaceOuterShapeLeadingEdge.cpp @@ -28,8 +28,6 @@ #include "Debugging.h" #include -#include -#include namespace tigl { @@ -47,31 +45,7 @@ PNamedShape CCPACSControlSurfaceOuterShapeLeadingEdge::GetLoft(PNamedShape wingC assert(shapeBox); // perform the boolean intersection of the flap box with the wing - - // Workaround for OpenCASCADE boolean intersection issues on LEDs: - // OCC seems to fail to create correct side faces when LEDs and the wing intersect - // along tangential or very thin regions (typical for LEDs). - // TED intersections seem to work fine. - // - // Solution: apply a small inward offset (epsilon) to the device box - // before performing the boolean intersection. - - gp_Vec epsilonVec = upDir; - epsilonVec *= -1e-6; - gp_Trsf trsf; - trsf.SetTranslation(epsilonVec); - - BRepBuilderAPI_Transform transformer(shapeBox->Shape(), trsf, true); - TopoDS_Shape offsetBox = transformer.Shape(); - - PNamedShape shapeBoxOffset(new CNamedShape(offsetBox, shapeBox->Name().c_str())); - - - BRepAlgoAPI_Common common(wingCleanShape->Shape(), shapeBoxOffset->Shape());; - common.Build(); - - TopoDS_Shape outerShapeTopo = common.Shape(); - PNamedShape outerShape(new CNamedShape(outerShapeTopo, shapeBox->Name().c_str())); + PNamedShape outerShape = CBopCommon(wingCleanShape, shapeBox); if (NeedsWingIntersection()) { return ControlSurfaceDeviceHelper::outerShapeGetLoft(shapeBox, outerShape, _uid); diff --git a/src/ducts/CCPACSDuct.cpp b/src/ducts/CCPACSDuct.cpp index cdb0422807..b3a16d2e5c 100644 --- a/src/ducts/CCPACSDuct.cpp +++ b/src/ducts/CCPACSDuct.cpp @@ -31,6 +31,8 @@ namespace tigl { CCPACSDuct::CCPACSDuct(CCPACSDucts* parent, CTiglUIDManager* uidMgr) : generated::CPACSDuct(parent, uidMgr) , CTiglRelativelyPositionedComponent(static_cast(nullptr), &m_transformation, &m_symmetry) + , loftUntrimmed(*this, &CCPACSDuct::BuildLoftUntrimmed) + , loftTrimmed(*this, &CCPACSDuct::BuildLoftTrimmed) {} CCPACSConfiguration& CCPACSDuct::GetConfiguration() const @@ -55,31 +57,48 @@ TiglGeometricComponentIntent CCPACSDuct::GetComponentIntent() const } PNamedShape CCPACSDuct::BuildLoft() const +{ + return *loftUntrimmed; +} + +void CCPACSDuct::BuildLoftUntrimmed(PNamedShape& cache) const +{ + BuildLoftImpl(cache, false); +} + +void CCPACSDuct::BuildLoftTrimmed(PNamedShape& cache) const +{ + BuildLoftImpl(cache, true); +} + +void CCPACSDuct::BuildLoftImpl(PNamedShape& cache, bool trim) const { TiglContinuity cont = m_segments.GetSegment(1).GetContinuity(); Standard_Boolean smooth = (cont == ::C0? false : true); CTiglMakeLoft lofter; - // add profiles + lofter.setMakeSolid(true); + lofter.setMakeSmooth(smooth); + // Only the trimmed loft is cut at the profiles; the untrimmed loft is a + // single continuous surface. + lofter.setEnableProfileCutting(trim); + for (int i=1; i <= m_segments.GetSegmentCount(); i++) { lofter.addProfiles(m_segments.GetSegment(i).GetStartWire()); } lofter.addProfiles(m_segments.GetSegment(m_segments.GetSegmentCount()).GetEndWire()); - // add guides lofter.addGuides(m_segments.GetGuideCurveWires()); - lofter.setMakeSolid(true); - lofter.setMakeSmooth(smooth); - - TopoDS_Shape loftShape = lofter.Shape(); + TopoDS_Shape loftShape = lofter.Shape(); std::string loftName = GetUID(); std::string loftShortName = GetShortShapeName(); - PNamedShape loft(new CNamedShape(loftShape, loftName.c_str(), loftShortName.c_str())); - SetFaceTraits(loft); + cache = std::make_shared(loftShape, loftName.c_str(), loftShortName.c_str()); - return loft; + // The trimmed loft has one face group per segment, whereas the untrimmed + // loft's aerodynamic faces form a single continuous group. + SetFaceTraits(cache, trim ? m_segments.GetSegmentCount() : 1); } // get short name for loft @@ -100,8 +119,12 @@ std::string CCPACSDuct::GetShortShapeName() const return "UNKNOWN"; } -void CCPACSDuct::SetFaceTraits (PNamedShape loft) const +void CCPACSDuct::SetFaceTraits(PNamedShape loft, int nSegments) const { + // Face layout: [aerodynamic faces][optional symmetry faces][front/rear caps]. + // For the trimmed loft the aerodynamic (and symmetry) faces are grouped per + // segment (nSegments > 1); for the untrimmed loft they form a single group + // (nSegments == 1). int nFacesTotal = GetNumberOfFaces(loft->Shape()); int nFacesAero = nFacesTotal; bool hasSymmetryPlane = GetNumberOfEdges(m_segments.GetSegment(1).GetEndWire()) > 1; @@ -119,10 +142,7 @@ void CCPACSDuct::SetFaceTraits (PNamedShape loft) const nFacesAero-=1; } - // if we have a smooth surface, the whole fuslage is treatet as one segment - int nSegments = m_segments.GetSegmentCount(); - - int facesPerSegment = nFacesAero/ nSegments; + int facesPerSegment = FacesPerSegment(nFacesAero, nSegments); int iFaceTotal = 0; int nSymmetryFaces = (int) hasSymmetryPlane; @@ -135,19 +155,34 @@ void CCPACSDuct::SetFaceTraits (PNamedShape loft) const } } - // set the caps int iFace = 2; - for (;iFaceTotal < nFacesTotal; ++iFaceTotal) { - loft->FaceTraits(iFaceTotal).SetName(names[iFace++].c_str()); + for (;iFaceTotal < nFacesTotal; ++iFaceTotal, ++iFace) { + if (iFace < (int)names.size()) { + loft->FaceTraits(iFaceTotal).SetName(names[iFace].c_str()); + } } } +PNamedShape CCPACSDuct::GetUntrimmedLoft() const +{ + // GetLoft() returns the untrimmed loft, so this is the untrimmed loft by + // definition. + return GetLoft(); +} + +PNamedShape CCPACSDuct::GetTrimmedLoft() const +{ + return *loftTrimmed; +} + void CCPACSDuct::RegisterInvalidationCallback(std::function const& fn){ invalidationCallbacks.push_back(fn); } void CCPACSDuct::InvalidateImpl(const boost::optional&) const { + loftTrimmed.clear(); + loftUntrimmed.clear(); CTiglAbstractGeometricComponent::Reset(); for (auto const& invalidator: invalidationCallbacks) { invalidator(); diff --git a/src/ducts/CCPACSDuct.h b/src/ducts/CCPACSDuct.h index 02863cffac..e6593cb61b 100644 --- a/src/ducts/CCPACSDuct.h +++ b/src/ducts/CCPACSDuct.h @@ -45,6 +45,9 @@ class CCPACSDuct : public generated::CPACSDuct, public CTiglRelativelyPositioned //as a callback. TIGL_EXPORT void RegisterInvalidationCallback(std::function const&); + TIGL_EXPORT PNamedShape GetTrimmedLoft() const override; + TIGL_EXPORT PNamedShape GetUntrimmedLoft() const; + protected: PNamedShape BuildLoft() const override; @@ -55,10 +58,18 @@ class CCPACSDuct : public generated::CPACSDuct, public CTiglRelativelyPositioned // get short name for loft std::string GetShortShapeName() const; - void SetFaceTraits (PNamedShape loft) const; + // Names the loft's faces. nSegments controls whether the aerodynamic faces + // are grouped per segment (trimmed loft) or as a single group (untrimmed loft). + void SetFaceTraits (PNamedShape loft, int nSegments) const; std::vector> invalidationCallbacks; + mutable Cache loftUntrimmed; /**< Duct surface, untrimmed (without UV cuts at profiles) */ + mutable Cache loftTrimmed; /**< Duct surface, trimmed (with UV cuts at profiles) */ + + void BuildLoftTrimmed(PNamedShape& cache) const; + void BuildLoftUntrimmed(PNamedShape& cache) const; + void BuildLoftImpl(PNamedShape& cache, bool trim) const; }; } diff --git a/src/engine_pylon/CCPACSEnginePylon.cpp b/src/engine_pylon/CCPACSEnginePylon.cpp index be7797353c..a9fff85b47 100644 --- a/src/engine_pylon/CCPACSEnginePylon.cpp +++ b/src/engine_pylon/CCPACSEnginePylon.cpp @@ -29,6 +29,8 @@ namespace tigl CCPACSEnginePylon::CCPACSEnginePylon(CCPACSEnginePylons* parent, CTiglUIDManager* uidMgr) : generated::CPACSEnginePylon(parent, uidMgr) , CTiglRelativelyPositionedComponent(&m_parentUID, &m_transformation, &m_symmetry) + , loftUntrimmed(*this, &CCPACSEnginePylon::BuildLoftUntrimmed) + , loftTrimmed(*this, &CCPACSEnginePylon::BuildLoftTrimmed) { } @@ -39,14 +41,44 @@ std::string CCPACSEnginePylon::GetDefaultedUID() const void CCPACSEnginePylon::InvalidateImpl(const boost::optional& source) const { + // Invalidate both trimmed and untrimmed loft caches + loftTrimmed.clear(); + loftUntrimmed.clear(); CTiglAbstractGeometricComponent::Reset(); } +// Untrimmed loft – default behavior (no profile cutting) PNamedShape CCPACSEnginePylon::BuildLoft() const { - CTiglEnginePylonBuilder builder(*this); + // Delegates to untrimmed cache + return *loftUntrimmed; +} + +void CCPACSEnginePylon::BuildLoftImpl(PNamedShape& cache, bool trim) const +{ + CTiglEnginePylonBuilder builder(*this, trim); + cache = builder.BuildShape(); +} + +void CCPACSEnginePylon::BuildLoftUntrimmed(PNamedShape& cache) const +{ + BuildLoftImpl(cache, false); +} - return builder.BuildShape(); +void CCPACSEnginePylon::BuildLoftTrimmed(PNamedShape& cache) const +{ + BuildLoftImpl(cache, true); +} + +PNamedShape CCPACSEnginePylon::GetTrimmedLoft() const +{ + return *loftTrimmed; +} + +PNamedShape CCPACSEnginePylon::GetUntrimmedLoft() const +{ + // Alias for the default untrimmed loft + return GetLoft(); } void CCPACSEnginePylon::SetSymmetryAxis(const TiglSymmetryAxis& axis) diff --git a/src/engine_pylon/CCPACSEnginePylon.h b/src/engine_pylon/CCPACSEnginePylon.h index bef71e9ca6..e082636bc5 100644 --- a/src/engine_pylon/CCPACSEnginePylon.h +++ b/src/engine_pylon/CCPACSEnginePylon.h @@ -20,6 +20,7 @@ #include "generated/CPACSEnginePylon.h" #include "CTiglRelativelyPositionedComponent.h" +#include "Cache.h" #include "tigl_internal.h" namespace tigl { class CCPACSConfiguration; } @@ -49,11 +50,27 @@ class CCPACSEnginePylon : public generated::CPACSEnginePylon, public CTiglRelati TIGL_EXPORT bool HasLoft() const; protected: + // Legacy untrimmed loft (default behavior) virtual PNamedShape BuildLoft() const override; + // New API – trimmed/untrimmed caches + void BuildLoftImpl(PNamedShape& cache, bool trim) const; + void BuildLoftTrimmed(PNamedShape& cache) const; + void BuildLoftUntrimmed(PNamedShape& cache) const; + +public: + // Returns the trimmed loft (UV cuts at profile positions) + TIGL_EXPORT PNamedShape GetTrimmedLoft() const override; + // Returns the untrimmed loft (delegates to GetLoft()) + TIGL_EXPORT PNamedShape GetUntrimmedLoft() const; + private: void InvalidateImpl(const boost::optional& source) const override; + // Caches for trimmed/untrimmed geometry + mutable Cache loftUntrimmed; /**< Engine pylon surface, untrimmed (without UV cuts at profiles) */ + mutable Cache loftTrimmed; /**< Engine pylon surface, trimmed (with UV cuts at profiles) */ + }; } // end namespace tigl diff --git a/src/engine_pylon/CTiglEnginePylonBuilder.cpp b/src/engine_pylon/CTiglEnginePylonBuilder.cpp index 92989ef429..9b872932e8 100644 --- a/src/engine_pylon/CTiglEnginePylonBuilder.cpp +++ b/src/engine_pylon/CTiglEnginePylonBuilder.cpp @@ -23,6 +23,7 @@ PNamedShape CTiglEnginePylonBuilder::BuildShape() CTiglMakeLoft lofter; lofter.setMakeSolid(true); lofter.setMakeSmooth(true); + lofter.setEnableProfileCutting(_enableProfileCutting); for (int i=1; i <= segments->GetSegmentCount(); i++) { const TopoDS_Shape& startWire = segments->GetSegment(i).GetInnerWire(); diff --git a/src/engine_pylon/CTiglEnginePylonBuilder.h b/src/engine_pylon/CTiglEnginePylonBuilder.h index 8821d84b39..3ad30903c2 100644 --- a/src/engine_pylon/CTiglEnginePylonBuilder.h +++ b/src/engine_pylon/CTiglEnginePylonBuilder.h @@ -11,8 +11,8 @@ namespace tigl class CTiglEnginePylonBuilder { public: - CTiglEnginePylonBuilder(const CCPACSEnginePylon& pylon) - : m_pylon(pylon) + CTiglEnginePylonBuilder(const CCPACSEnginePylon& pylon, bool enableProfileCutting = false) + : m_pylon(pylon), _enableProfileCutting(enableProfileCutting) {} TIGL_EXPORT operator PNamedShape(); @@ -21,6 +21,7 @@ class CTiglEnginePylonBuilder private: const CCPACSEnginePylon& m_pylon; + bool _enableProfileCutting; }; } // namespace tigl diff --git a/src/fuelTanks/CCPACSVessel.cpp b/src/fuelTanks/CCPACSVessel.cpp index 5c8fbfe897..408ece3f45 100644 --- a/src/fuelTanks/CCPACSVessel.cpp +++ b/src/fuelTanks/CCPACSVessel.cpp @@ -27,6 +27,7 @@ #include "CNamedShape.h" #include "CTiglTopoAlgorithms.h" #include "tiglcommonfunctions.h" +#include "CTiglLogging.h" #include "CCPACSFuelTank.h" #include "generated/CPACSFuelTanks.h" #include "generated/CPACSDomeType.h" @@ -56,6 +57,8 @@ namespace tigl CCPACSVessel::CCPACSVessel(CCPACSVessels* parent, CTiglUIDManager* uidMgr) : generated::CPACSVessel(parent, uidMgr) , CTiglRelativelyPositionedComponent(GetParent()->GetParent(), &m_transformation) + , loftUntrimmed(*this, &CCPACSVessel::BuildLoftUntrimmed) + , loftTrimmed(*this, &CCPACSVessel::BuildLoftTrimmed) { m_transformation.setScalingType(ABS_LOCAL); m_transformation.setRotationType(ABS_LOCAL); @@ -367,7 +370,7 @@ TopoDS_Edge CCPACSVessel::IsotensoidContour::ToEdge() const return BRepBuilderAPI_MakeEdge(ToBSpline()); } -void CCPACSVessel::BuildShapeFromSegments(TopoDS_Shape& loftShape) const +void CCPACSVessel::BuildShapeFromSegments(TopoDS_Shape& loftShape, bool trim) const { const auto& segments = m_segments_choice1.get(); TiglContinuity cont = segments.GetSegment(1).GetContinuity(); @@ -385,6 +388,7 @@ void CCPACSVessel::BuildShapeFromSegments(TopoDS_Shape& loftShape) const lofter.setMakeSolid(true); lofter.setMakeSmooth(smooth); + lofter.setEnableProfileCutting(trim); loftShape = lofter.Shape(); } @@ -581,31 +585,71 @@ void CCPACSVessel::BuildShapeFromSimpleParameters(TopoDS_Shape& loftShape) const loftShape = TransformedShape; } -PNamedShape CCPACSVessel::BuildLoft() const +void CCPACSVessel::BuildLoftImpl(PNamedShape& cache, bool trim) const { TopoDS_Shape loftShape; std::string loftName = GetUID(); std::string loftShortName = GetShortShapeName(); if (m_sections_choice1) { - BuildShapeFromSegments(loftShape); - PNamedShape loft(new CNamedShape(loftShape, loftName.c_str(), loftShortName.c_str())); - SetFaceTraitsFromSegments(loft); - return loft; + BuildShapeFromSegments(loftShape, trim); + cache = std::make_shared(loftShape, loftName.c_str(), loftShortName.c_str()); + // The trimmed loft keeps one face per profile segment and is only used + // internally, so it is not named. The untrimmed loft is the public loft + // and gets proper face traits. + if (!trim) { + SetFaceTraitsFromSegments(cache); + } } else if (m_domeType_choice2) { - BuildShapeFromSimpleParameters(loftShape); - PNamedShape loft(new CNamedShape(loftShape, loftName.c_str(), loftShortName.c_str())); - SetFaceTraitsFromParams(loft); - return loft; + // Vessels specified via design parameters are not trimmed at profiles, + // so the trimmed loft stays null (see GetTrimmedLoft for the fallback). + if (!trim) { + BuildShapeFromSimpleParameters(loftShape); + cache = std::make_shared(loftShape, loftName.c_str(), loftShortName.c_str()); + SetFaceTraitsFromParams(cache); + } } - else { + else if (!trim) { throw CTiglError("No valid combination of segments and sections or parametric specification for lofting of " "tank vessel available.", TIGL_ERROR); } } +void CCPACSVessel::BuildLoftUntrimmed(PNamedShape& cache) const +{ + BuildLoftImpl(cache, false); +} + +void CCPACSVessel::BuildLoftTrimmed(PNamedShape& cache) const +{ + BuildLoftImpl(cache, true); +} + +PNamedShape CCPACSVessel::BuildLoft() const +{ + return *loftUntrimmed; +} + +PNamedShape CCPACSVessel::GetTrimmedLoft() const +{ + PNamedShape trimmedLoft = *loftTrimmed; + if (!trimmedLoft) { + // Vessels specified via parametric design parameters (i.e. not built from + // segments) have no trimmed loft (see BuildLoftTrimmed). Fall back to the + // untrimmed loft so that callers always receive a valid shape and never a + // null PNamedShape. + return GetUntrimmedLoft(); + } + return trimmedLoft; +} + +PNamedShape CCPACSVessel::GetUntrimmedLoft() const +{ + return GetLoft(); +} + CCPACSGuideCurve& CCPACSVessel::GetGuideCurveSegment(std::string uid) { return const_cast(static_cast(*this).GetGuideCurveSegment(uid)); @@ -683,36 +727,45 @@ void CCPACSVessel::SetFaceTraitsFromSegments(PNamedShape loft) const int nFacesAero = nFacesTotal; auto& segments = m_segments_choice1.get(); - int nSegments = segments.GetSegmentCount(); bool hasSymmetryPlane = GetNumberOfEdges(segments.GetSegment(1).GetEndWire()) > 1; - std::array names = {loft->Name(), "symmetry", "Front", "Rear"}; + std::vector names = {loft->Name(), "symmetry", "Front", "Rear"}; + // Count and strip the front/rear cap faces that close the solid. A cap is only + // present if the corresponding end profile is not degenerated to a point. if (!CTiglTopoAlgorithms::IsDegenerated(segments.GetSegment(1).GetStartWire())) { - nFacesAero--; + nFacesAero -= 1; } - if (!CTiglTopoAlgorithms::IsDegenerated(segments.GetSegment(nSegments).GetEndWire())) { - nFacesAero--; + if (!CTiglTopoAlgorithms::IsDegenerated(segments.GetSegment(segments.GetSegmentCount()).GetEndWire())) { + nFacesAero -= 1; } - int facesPerSegment = nFacesAero / nSegments; - int iFaceTotal = 0; - int nSymmetryFaces = hasSymmetryPlane ? 1 : 0; + int nSymmetryFaces = (int) hasSymmetryPlane; - for (int iSegment = 0; iSegment < nSegments; ++iSegment) { - for (int iFace = 0; iFace < facesPerSegment - nSymmetryFaces; ++iFace) { - loft->FaceTraits(iFaceTotal++).SetName(names[0].c_str()); - } - for (int iFace = 0; iFace < nSymmetryFaces; ++iFace) { - loft->FaceTraits(iFaceTotal++).SetName(names[1].c_str()); + // The untrimmed vessel loft is a single group that may contain several aero + // faces (e.g. one per guide curve sector) plus an optional symmetry face, + // followed by the front/rear caps. + if (nFacesAero < 1 + nSymmetryFaces) { + LOG(WARNING) << "Faces of the vessel loft cannot be named properly."; + for (int iFace = 0; iFace < nFacesTotal; ++iFace) { + loft->FaceTraits(iFace).SetName(names[0].c_str()); } + return; + } + + int iFaceTotal = 0; + for (int iFace = 0; iFace < nFacesAero - nSymmetryFaces; ++iFace) { + loft->FaceTraits(iFaceTotal++).SetName(names[0].c_str()); + } + for (int iFace = 0; iFace < nSymmetryFaces; ++iFace) { + loft->FaceTraits(iFaceTotal++).SetName(names[1].c_str()); } - // Front and rear caps - int iFace = 2; + // set the caps (front first, then rear) + int iCapName = 2; for (; iFaceTotal < nFacesTotal; ++iFaceTotal) { - loft->FaceTraits(iFaceTotal).SetName(names[iFace++].c_str()); + loft->FaceTraits(iFaceTotal).SetName(names[iCapName++].c_str()); } } @@ -734,6 +787,8 @@ void CCPACSVessel::SetFaceTraitsFromParams(PNamedShape loft) const void CCPACSVessel::InvalidateImpl(const boost::optional&) const { loft.clear(); + loftTrimmed.clear(); + loftUntrimmed.clear(); if (m_segments_choice1) { m_segments_choice1.get().Invalidate(); } diff --git a/src/fuelTanks/CCPACSVessel.h b/src/fuelTanks/CCPACSVessel.h index 1f3cfaa63f..20e13f9e29 100644 --- a/src/fuelTanks/CCPACSVessel.h +++ b/src/fuelTanks/CCPACSVessel.h @@ -96,10 +96,22 @@ class CCPACSVessel : public generated::CPACSVessel, public CTiglRelativelyPositi // Check whether the vessel has isotensoid dome TIGL_EXPORT bool HasIsotensoidDome() const; + // Returns the trimmed loft. For vessels specified via design parameters + // (IsVesselViaDesignParameters == true) no trimmed loft exists, so this + // falls back to the untrimmed loft and always returns a valid shape. + TIGL_EXPORT PNamedShape GetTrimmedLoft() const override; + + // Retunrs the untrimmed loft (delegates to GetLoft) + TIGL_EXPORT PNamedShape GetUntrimmedLoft() const; + protected: // Build the loft PNamedShape BuildLoft() const override; + void BuildLoftImpl(PNamedShape& cache, bool trim) const; + void BuildLoftUntrimmed(PNamedShape& cache) const; + void BuildLoftTrimmed(PNamedShape& cache) const; + // Set the face traits void SetFaceTraitsFromSegments(PNamedShape loft) const; void SetFaceTraitsFromParams(PNamedShape loft) const; @@ -117,7 +129,7 @@ class CCPACSVessel : public generated::CPACSVessel, public CTiglRelativelyPositi // Get short name for loft std::string GetShortShapeName() const; - void BuildShapeFromSegments(TopoDS_Shape& loftShape) const; + void BuildShapeFromSegments(TopoDS_Shape& loftShape, bool trim=false) const; void BuildShapeFromSimpleParameters(TopoDS_Shape& loftShape) const; void BuildVesselWire(std::vector& edges, BRepBuilderAPI_MakeWire& wire) const; @@ -125,6 +137,9 @@ class CCPACSVessel : public generated::CPACSVessel, public CTiglRelativelyPositi void BuildVesselWireTorispherical(BRepBuilderAPI_MakeWire& wire) const; void BuildVesselWireIsotensoid(BRepBuilderAPI_MakeWire& wire) const; + Cache loftUntrimmed; /**< Clean vessel surface, untrimmed (without UV cuts at profiles) */ + Cache loftTrimmed; /**< Clean vessel surface, trimmed (with UV cuts at profiles) */ + /** * @brief Approximated contour of an isotensoid dome section. * diff --git a/src/fuselage/CCPACSFuselage.cpp b/src/fuselage/CCPACSFuselage.cpp index 82d7d6914f..a23ed1c768 100644 --- a/src/fuselage/CCPACSFuselage.cpp +++ b/src/fuselage/CCPACSFuselage.cpp @@ -79,7 +79,8 @@ namespace tigl CCPACSFuselage::CCPACSFuselage(CCPACSFuselages* parent, CTiglUIDManager* uidMgr) : generated::CPACSFuselage(parent, uidMgr) , CTiglRelativelyPositionedComponent(&m_parentUID, &m_transformation, &m_symmetry) - , cleanLoft(*this, &CCPACSFuselage::BuildCleanLoft) + , cleanLoftUntrimmed(*this, &CCPACSFuselage::BuildCleanLoftUntrimmed) + , cleanLoftTrimmed(*this, &CCPACSFuselage::BuildCleanLoftTrimmed) , boundingBoxHeightWidthCache(*this, &CCPACSFuselage::BuildBoundingBoxHeightWidth) , fuselageHelper(*this, &CCPACSFuselage::SetFuselageHelper) { @@ -106,7 +107,8 @@ CCPACSFuselage::~CCPACSFuselage() // Invalidates internal state void CCPACSFuselage::InvalidateImpl(const boost::optional& /*source*/) const { - cleanLoft.clear(); + cleanLoftUntrimmed.clear(); + cleanLoftTrimmed.clear(); loft.clear(); boundingBoxHeightWidthCache.clear(); m_segments.Invalidate(); @@ -233,7 +235,7 @@ std::string CCPACSFuselage::GetShortShapeName () const return "UNKNOWN"; } -void CCPACSFuselage::SetFaceTraits (PNamedShape loft) const +void CCPACSFuselage::SetFaceTraits (PNamedShape loft, int nSegments) const { int nFacesTotal = GetNumberOfFaces(loft->Shape()); int nFacesAero = nFacesTotal; @@ -245,6 +247,8 @@ void CCPACSFuselage::SetFaceTraits (PNamedShape loft) const names.push_back("Front"); names.push_back("Rear"); + // Count and strip the front/rear cap faces that close the solid. A cap is only + // present if the corresponding end profile is not degenerated to a point. if (!CTiglTopoAlgorithms::IsDegenerated(GetSegment(1).GetStartWire())) { nFacesAero-=1; } @@ -252,13 +256,23 @@ void CCPACSFuselage::SetFaceTraits (PNamedShape loft) const nFacesAero-=1; } - // if we have a smooth surface, the whole fuslage is treatet as one segment - int nSegments = this->GetSegmentCount(); + int nSymmetryFaces = (int) hasSymmetryPlane; - int facesPerSegment = nFacesAero/ nSegments; + // The remaining faces are the aerodynamic faces plus (per segment) an optional + // symmetry face. The untrimmed loft is not cut at the profiles, so it forms a + // single group (nSegments == 1) that may still contain several aero faces (e.g. + // one per guide curve sector); the trimmed loft has one group per segment. + if (nFacesAero < nSegments * (1 + nSymmetryFaces)) { + LOG(WARNING) << "Faces of the fuselage loft cannot be named properly."; + for (int iFace = 0; iFace < nFacesTotal; ++iFace) { + loft->FaceTraits(iFace).SetName(names[0].c_str()); + } + return; + } + + int facesPerSegment = FacesPerSegment(nFacesAero, nSegments); int iFaceTotal = 0; - int nSymmetryFaces = (int) hasSymmetryPlane; for (int iSegment = 0; iSegment < nSegments; ++iSegment) { for (int iFace = 0; iFace < facesPerSegment - nSymmetryFaces; ++iFace) { loft->FaceTraits(iFaceTotal++).SetName(names[0].c_str()); @@ -268,10 +282,10 @@ void CCPACSFuselage::SetFaceTraits (PNamedShape loft) const } } - // set the caps - int iFace = 2; + // set the caps (front first, then rear) + int iCapName = 2; for (;iFaceTotal < nFacesTotal; ++iFaceTotal) { - loft->FaceTraits(iFaceTotal).SetName(names[iFace++].c_str()); + loft->FaceTraits(iFaceTotal).SetName(names[iCapName++].c_str()); } } @@ -279,36 +293,60 @@ void CCPACSFuselage::SetFaceTraits (PNamedShape loft) const PNamedShape CCPACSFuselage::BuildLoft() const { if (!GetConfiguration().HasDucts()) { - return *cleanLoft; + return *cleanLoftUntrimmed; } - return GetConfiguration().GetDucts()->LoftWithDuctCutouts(*cleanLoft, GetUID()); + return GetConfiguration().GetDucts()->LoftWithDuctCutouts(*cleanLoftUntrimmed, GetUID()); +} + +PNamedShape CCPACSFuselage::GetUntrimmedLoft() const +{ + // GetLoft() already returns the untrimmed loft (with duct cutouts applied + // if present), so this is the untrimmed loft by definition. + return GetLoft(); +} + +PNamedShape CCPACSFuselage::GetTrimmedLoft() const +{ + if (!GetConfiguration().HasDucts()) { + return *cleanLoftTrimmed; + } + return GetConfiguration().GetDucts()->LoftWithDuctCutouts(*cleanLoftTrimmed, GetUID()); } -void CCPACSFuselage::BuildCleanLoft(PNamedShape& cache) const +void CCPACSFuselage::BuildCleanLoftImpl(PNamedShape& cache, bool trim) const { TiglContinuity cont = m_segments.GetSegment(1).GetContinuity(); Standard_Boolean smooth = (cont == ::C0? false : true); CTiglMakeLoft lofter; - // add profiles + lofter.setMakeSolid(true); + lofter.setMakeSmooth(smooth); + lofter.setEnableProfileCutting(trim); + for (int i=1; i <= m_segments.GetSegmentCount(); i++) { lofter.addProfiles(m_segments.GetSegment(i).GetStartWire()); } lofter.addProfiles(m_segments.GetSegment(m_segments.GetSegmentCount()).GetEndWire()); - // add guides lofter.addGuides(m_segments.GetGuideCurveWires()); - lofter.setMakeSolid(true); - lofter.setMakeSmooth(smooth); - TopoDS_Shape loftShape = lofter.Shape(); std::string loftName = GetUID(); std::string loftShortName = GetShortShapeName(); cache = std::make_shared(loftShape, loftName.c_str(), loftShortName.c_str()); - SetFaceTraits(cache); + SetFaceTraits(cache, trim ? m_segments.GetSegmentCount() : 1); +} + +void CCPACSFuselage::BuildCleanLoftUntrimmed(PNamedShape& cache) const +{ + BuildCleanLoftImpl(cache, false); +} + +void CCPACSFuselage::BuildCleanLoftTrimmed(PNamedShape& cache) const +{ + BuildCleanLoftImpl(cache, true); } // Get the positioning transformation for a given section index diff --git a/src/fuselage/CCPACSFuselage.h b/src/fuselage/CCPACSFuselage.h index 95c6148a92..867828b69c 100644 --- a/src/fuselage/CCPACSFuselage.h +++ b/src/fuselage/CCPACSFuselage.h @@ -260,16 +260,41 @@ class CCPACSFuselage : public generated::CPACSFuselage, public CTiglRelativelyPo */ TIGL_EXPORT std::vector GetAllUsedProfiles(); - /** - * Set the profile uid of all the section elements of this fuselage. - * @param profileUID ; the profile UID to use - */ + /** + * Set the profile uid of all the section elements of this fuselage. + * @param profileUID ; the profile UID to use + */ TIGL_EXPORT void SetAllProfiles(const std::string& profileUID); + /** + * @brief Returns the fuselage loft (untrimmed, i.e. without UV cuts at profile positions). + * This is the default loft returned by GetLoft(). + * @return PNamedShape + */ + TIGL_EXPORT PNamedShape GetUntrimmedLoft() const; + + /** + * @brief Returns the fuselage loft with UV cuts at profile positions. + * This is the legacy trimmed behavior. + * @return PNamedShape + */ + TIGL_EXPORT PNamedShape GetTrimmedLoft() const override; protected: - void BuildCleanLoft(PNamedShape& cache) const; + /** + * @brief Names the faces of the fuselage loft. + * + * The face layout is [aero faces][optional symmetry face] repeated @p nSegments + * times, followed by the optional front/rear cap faces. Pass nSegments == 1 for + * the untrimmed loft (one continuous aero/symmetry group) and the actual segment + * count for the trimmed loft (one group per segment). + */ + void SetFaceTraits(PNamedShape loft, int nSegments) const; + + void BuildCleanLoftImpl(PNamedShape& cache, bool trim) const; + void BuildCleanLoftUntrimmed(PNamedShape& cache) const; + void BuildCleanLoftTrimmed(PNamedShape& cache) const; // Cleanup routine void Cleanup(); @@ -277,8 +302,6 @@ class CCPACSFuselage : public generated::CPACSFuselage, public CTiglRelativelyPo // Adds all segments of this fuselage to one shape PNamedShape BuildLoft() const override; - void SetFaceTraits(PNamedShape loft) const; - void SetFuselageHelper(CTiglFuselageHelper& cache) const ; /** @@ -299,7 +322,8 @@ class CCPACSFuselage : public generated::CPACSFuselage, public CTiglRelativelyPo CCPACSConfiguration* configuration; /**< Parent configuration */ FusedElementsContainerType fusedElements; /**< Stores already fused segments */ - Cache cleanLoft; /**< Stores the loft with cutouts (e.g. ducts) */ + Cache cleanLoftUntrimmed; /**< Clean fuselage surface, untrimmed (without UV cuts at profiles) */ + Cache cleanLoftTrimmed; /**< Clean fuselage surface, trimmed (with UV cuts at profiles) */ Cache boundingBoxHeightWidthCache; TopoDS_Compound aCompound; diff --git a/src/fuselage/CCPACSFuselageProfile.cpp b/src/fuselage/CCPACSFuselageProfile.cpp index 43d140b3ae..672c04c3f3 100644 --- a/src/fuselage/CCPACSFuselageProfile.cpp +++ b/src/fuselage/CCPACSFuselageProfile.cpp @@ -276,7 +276,7 @@ void CCPACSFuselageProfile::BuildWiresPointList(WireCache& cache) const auto paramsVec = computeParams(occPoints, paramsMap, 0.5); - int max_iter = 5; + int max_iter = 10; CTiglApproxResult approxResult = approx.FitCurveOptimal(paramsVec, max_iter, approxErrFct); spline = approxResult.curve; diff --git a/src/fuselage/CCPACSFuselageSegment.cpp b/src/fuselage/CCPACSFuselageSegment.cpp index d32eb3ec15..e0d7406d8d 100644 --- a/src/fuselage/CCPACSFuselageSegment.cpp +++ b/src/fuselage/CCPACSFuselageSegment.cpp @@ -349,7 +349,7 @@ void CCPACSFuselageSegment::SetFaceTraits (PNamedShape loft) const int facesPerSegment = GetNumberOfLoftFaces(); int remainingFaces = nFaces - facesPerSegment; if (facesPerSegment == 0 || remainingFaces < 0 || remainingFaces > 2) { - LOG(WARNING) << "Fuselage segment faces cannot be names properly (maybe due to Guide Curves?)"; + LOG(WARNING) << "Fuselage segment faces cannot be named properly (maybe due to Guide Curves?)"; return; } @@ -382,7 +382,22 @@ PNamedShape CCPACSFuselageSegment::BuildLoft() const } else { // retrieve segment loft as subshape of the fuselage loft - PNamedShape fuselageLoft = GetParent()->GetParentComponent()->GetLoft(); + PNamedShape fuselageLoft; + if (GetParent()->IsParent()) { + fuselageLoft = GetParent()->GetParent()->GetTrimmedLoft(); + } + else if (GetParent()->IsParent()) { + fuselageLoft = GetParent()->GetParent()->GetTrimmedLoft(); + } + else if (GetParent()->IsParent()) { + fuselageLoft = GetParent()->GetParent()->GetTrimmedLoft(); + } + else if (GetParent()->IsParent()) { + throw CTiglError("CCPACSFuselageSegment::BuildLoft called on a CCPACSMultiSegmentShape. This is currently not supported."); + } + else { + throw CTiglError("Unknown parent type for CCPACSFuselageSegments."); + } TopoDS_Shell loftShell; BRep_Builder BB; @@ -394,10 +409,18 @@ PNamedShape CCPACSFuselageSegment::BuildLoft() const //determine the number of faces per segment int nFacesPerSegment = GetNumberOfLoftFaces(); + int nfaces = faceMap.Extent(); const int mySegmentIndex = GetSegmentIndex(); for (int i = 1; i <= nFacesPerSegment; ++i) { - BB.Add(loftShell, TopoDS::Face(faceMap(nFacesPerSegment*(mySegmentIndex-1) + i))); + int faceIndex = nFacesPerSegment*(mySegmentIndex-1) + i; + if (faceIndex < 1 || faceIndex > nfaces) { + LOG(ERROR) << "CCPACSFuselageSegment::BuildLoft: computed face index " << faceIndex + << " is out of range [1, " << nfaces << "] for segment \"" << GetUID() + << "\". The trimmed parent loft does not contain the expected number of faces."; + throw CTiglError("CCPACSFuselageSegment::BuildLoft: face index out of range for segment \"" + GetUID() + "\".", TIGL_ERROR); + } + BB.Add(loftShell, TopoDS::Face(faceMap(faceIndex))); } //close the shell with sidecaps and make them a solid @@ -795,7 +818,7 @@ gp_Pnt CCPACSFuselageSegment::GetPointOnXPlane(double eta, double xpos, int poin // Gets the wire on the loft at a given eta TopoDS_Shape CCPACSFuselageSegment::getWireOnLoft(double eta) { - + PNamedShape loft; TopoDS_Shape s = GetFacesByName(GetLoft(), GetUID()); BRepBuilderAPI_MakeWire wireMaker; @@ -957,7 +980,6 @@ TIGL_EXPORT int CCPACSFuselageSegment::GetNumberOfLoftFaces() const nfaces-=1; } - int facesPerSegment = nfaces / nSegments; - return facesPerSegment; + return FacesPerSegment(nfaces, nSegments); } } // end namespace tigl diff --git a/src/geometry/CTiglMakeLoft.cpp b/src/geometry/CTiglMakeLoft.cpp index b30ace58d1..d3d4b03665 100644 --- a/src/geometry/CTiglMakeLoft.cpp +++ b/src/geometry/CTiglMakeLoft.cpp @@ -74,7 +74,6 @@ CTiglMakeLoft::CTiglMakeLoft(const TopoDS_Shape& profiles, const TopoDS_Shape& g _hasPerformed = false; _result.Nullify(); _myTolerance = tolerance; - _myTolerance = tolerance; _mySameKnotTolerance = sameKnotTolerance; addProfiles(profiles); addGuides(guides); @@ -109,7 +108,6 @@ void CTiglMakeLoft::addGuides(const TopoDS_Shape &guides) TopoDS_Shape &CTiglMakeLoft::Shape() { Perform(); - return _result; } @@ -153,6 +151,11 @@ void CTiglMakeLoft::setMakeSmooth(bool enabled) _makeSmooth = enabled; } +void CTiglMakeLoft::setEnableProfileCutting(bool enabled) +{ + _enableProfileCutting = enabled; +} + /** * @brief Builds the loft using profiles and guide curves */ @@ -293,16 +296,25 @@ void CTiglMakeLoft::makeLoftWithoutGuides() builder.Add(faces, BRepBuilderAPI_MakeFace(surface, 1e-6).Face()); } - _result = tigl::CTiglTopoAlgorithms::CutShellAtUVParameters(faces, {}, vparams); - - // make sure the order is the same as for the COONS Patch algorithm - _result = ResortFaces(_result, nEdgesPerProfile, static_cast(vparams.size()-1)); - _result = tigl::CTiglTopoAlgorithms::CutShellAtKinks(_result); + + if (_enableProfileCutting) { + _result = tigl::CTiglTopoAlgorithms::CutShellAtUVParameters(faces, {}, vparams); + _result = ResortFaces(_result, nEdgesPerProfile, static_cast(vparams.size()-1)); + _result = tigl::CTiglTopoAlgorithms::CutShellAtKinks(_result); + } else { + // For untrimmed loft, use faces directly without cutting + _result = faces; + } + CloseShape(); } void CTiglMakeLoft::CloseShape() { + int nFacesResult = 0; + for (TopExp_Explorer exp(_result, TopAbs_FACE); exp.More(); exp.Next()) { + nFacesResult++; + } tigl::CTiglPatchShell patcher(_result, _myTolerance); Standard_Boolean vClosed = (profiles[0].IsSame(profiles.back())); if ( !vClosed && _makeSolid ) { diff --git a/src/geometry/CTiglMakeLoft.h b/src/geometry/CTiglMakeLoft.h index cc0273f85c..73995ef282 100644 --- a/src/geometry/CTiglMakeLoft.h +++ b/src/geometry/CTiglMakeLoft.h @@ -66,7 +66,17 @@ class CTiglMakeLoft * @param enabled Set to true, if smoothing should be enabled. */ TIGL_EXPORT void setMakeSmooth(bool enabled); - + + /** + * @brief setEnableProfileCutting enables or disables cutting the resulting + * shell at the profile (section) positions and kink locations. When disabled, + * the loft is returned as a continuous surface without UV cuts at profiles. + * Default is false (cutting disabled). + * + * @param enabled Set to true to enable profile cutting (original behavior). + */ + TIGL_EXPORT void setEnableProfileCutting(bool enabled); + TIGL_EXPORT TopoDS_Shape& Shape(); TIGL_EXPORT operator TopoDS_Shape& (); @@ -106,6 +116,7 @@ class CTiglMakeLoft std::vector uparams, vparams; bool _hasPerformed, _makeSolid; bool _makeSmooth = false; + bool _enableProfileCutting = false; TopoDS_Shape _result; }; diff --git a/src/geometry/CTiglPatchShell.cpp b/src/geometry/CTiglPatchShell.cpp index 86226b5efd..2343349b69 100644 --- a/src/geometry/CTiglPatchShell.cpp +++ b/src/geometry/CTiglPatchShell.cpp @@ -18,6 +18,7 @@ #include "CTiglPatchShell.h" #include "CTiglError.h" +#include "tiglcommonfunctions.h" #include @@ -45,8 +46,8 @@ namespace namespace tigl { CTiglPatchShell::CTiglPatchShell(TopoDS_Shape const& shell, double tol) - : _inputShell(shell) - , _tolerance(tol) + : _inputShell(shell) + , _tolerance(tol) {} void CTiglPatchShell::AddSideCap(TopoDS_Wire const& boundaryWire) @@ -112,6 +113,11 @@ TopoDS_Shape CTiglPatchShell::PatchedShape() void CTiglPatchShell::Perform() { + int nFacesInput = GetNumberOfFaces(_inputShell); + if (nFacesInput == 0) { + throw CTiglError("Cannot patch a shape with no faces", TIGL_ERROR); + } + TopoDS_Shape shell = MakeShells(_inputShell, _tolerance); if (_sidecaps.size()>0) { @@ -144,6 +150,12 @@ void CTiglPatchShell::Perform() throw CTiglError("Cannot make a solid out of the shell. Is the base type correct?", TIGL_ERROR); } + // Check if solid is empty + TopExp_Explorer exp(solid, TopAbs_SHELL); + if (!exp.More()) { + throw CTiglError("Cannot make a solid from an empty shell", TIGL_ERROR); + } + // verify the orientation of the solid BRepClass3d_SolidClassifier clas3d(solid); clas3d.PerformInfinitePoint(Precision::Confusion()); @@ -171,7 +183,28 @@ TopoDS_Shell MakeShells(TopoDS_Shape const& shell, const Standard_Real tol) if (shell.IsNull()) { throw tigl::CTiglError("Loft is not build", TIGL_ERROR); } - + + // Count faces in shell + int nFaces = 0; + for (TopExp_Explorer exp(shell, TopAbs_FACE); exp.More(); exp.Next()) { + nFaces++; + } + + if (nFaces == 0) { + BRep_Builder B; + TopoDS_Shell shellFinal; + B.MakeShell(shellFinal); + return shellFinal; + } + + if (nFaces == 1) { + BRep_Builder B; + TopoDS_Shell shellFinal; + B.MakeShell(shellFinal); + B.Add(shellFinal, TopoDS::Face(TopExp_Explorer(shell, TopAbs_FACE).Current())); + return shellFinal; + } + try { BRepBuilderAPI_Sewing BB(tol); BB.Add(shell); @@ -182,6 +215,18 @@ TopoDS_Shell MakeShells(TopoDS_Shape const& shell, const Standard_Real tol) if ( shellClosed.ShapeType() != TopAbs_SHELL ) { if ( shellClosed.ShapeType() != TopAbs_FACE) { + if ( shellClosed.ShapeType() == TopAbs_COMPOUND ) { + BRep_Builder B; + TopoDS_Shell shellFinal; + B.MakeShell(shellFinal); + TopExp_Explorer exp(shellClosed, TopAbs_FACE); + int faceCount = 0; + for (; exp.More(); exp.Next()) { + B.Add(shellFinal, TopoDS::Face(exp.Current())); + faceCount++; + } + return shellFinal; + } throw tigl::CTiglError("Cannot patch a shape that is neither a shell nor a face"); } diff --git a/src/geometry/ITiglGeometricComponent.h b/src/geometry/ITiglGeometricComponent.h index 388c5a2d44..e53d029ad5 100644 --- a/src/geometry/ITiglGeometricComponent.h +++ b/src/geometry/ITiglGeometricComponent.h @@ -38,6 +38,8 @@ class ITiglGeometricComponent // Gets the loft of a geometric component TIGL_EXPORT virtual PNamedShape GetLoft() const = 0; + // Gets the trimmed loft (cut at profile and guide curve boundaries, cut a t kinks); defaults to untrimmed if not overridden + TIGL_EXPORT virtual PNamedShape GetTrimmedLoft() const { return GetLoft(); } // Returns the Geometric type of this component, e.g. Wing or Fuselage TIGL_EXPORT virtual TiglGeometricComponentType GetComponentType() const = 0; diff --git a/src/systems/CCPACSComponent.cpp b/src/systems/CCPACSComponent.cpp index b5601796e0..be77e7450e 100644 --- a/src/systems/CCPACSComponent.cpp +++ b/src/systems/CCPACSComponent.cpp @@ -260,6 +260,18 @@ PNamedShape CCPACSComponent::BuildLoft() const return GetTransformationMatrix().Transform(BuildLocalLoft()); } +PNamedShape CCPACSComponent::GetUntrimmedLoft() const +{ + // Default to current behavior (no trimming support for generic components) + return GetLoft(); +} + +PNamedShape CCPACSComponent::GetTrimmedLoft() const +{ + // Default to current behavior (no trimming support for generic components) + return GetLoft(); +} + void CCPACSComponent::BuildMass(MassCache& cache) const { const auto* massPtr = GetMassDescription(*m_uidMgr, GetSystemElementUID()); diff --git a/src/systems/CCPACSComponent.h b/src/systems/CCPACSComponent.h index d526fd2fe6..5593e8123c 100644 --- a/src/systems/CCPACSComponent.h +++ b/src/systems/CCPACSComponent.h @@ -181,6 +181,19 @@ class CCPACSComponent : public generated::CPACSComponent, public CTiglRelatively */ TIGL_EXPORT bool IsPositioned() const; + /** + * @brief Returns the component loft (untrimmed). + * For components referencing fuselages/wings, this returns the untrimmed loft. + * @return PNamedShape + */ + TIGL_EXPORT PNamedShape GetUntrimmedLoft() const; + + /** + * @brief Returns the component loft with UV cuts at profile positions (trimmed). + * @return PNamedShape + */ + TIGL_EXPORT PNamedShape GetTrimmedLoft() const override; + protected: virtual PNamedShape BuildLoft() const override; diff --git a/src/wing/CCPACSWing.cpp b/src/wing/CCPACSWing.cpp index 4d204d8440..cb98caecc4 100644 --- a/src/wing/CCPACSWing.cpp +++ b/src/wing/CCPACSWing.cpp @@ -98,7 +98,8 @@ CCPACSWing::CCPACSWing(CCPACSWings* parent, CTiglUIDManager* uidMgr) , CTiglRelativelyPositionedComponent(&m_parentUID, &m_transformation, &m_symmetry) , guideCurves(*this, &CCPACSWing::BuildGuideCurveWires) , wingShapeWithCutouts(*this, &CCPACSWing::BuildWingWithCutouts) - , wingCleanShape(*this, &CCPACSWing::BuildFusedSegments) + , wingCleanShapeUntrimmed(*this, &CCPACSWing::BuildFusedSegmentsUntrimmed) + , wingCleanShapeTrimmed(*this, &CCPACSWing::BuildFusedSegmentsTrimmed) , rebuildFusedSegWEdge(true) , rebuildShells(true) , buildFlaps(false) @@ -127,7 +128,8 @@ CCPACSWing::CCPACSWing(CCPACSRotorBlades* parent, CTiglUIDManager* uidMgr) , configuration(&parent->GetConfiguration()) , guideCurves(*this, &CCPACSWing::BuildGuideCurveWires) , wingShapeWithCutouts(*this, &CCPACSWing::BuildWingWithCutouts) - , wingCleanShape(*this, &CCPACSWing::BuildFusedSegments) + , wingCleanShapeUntrimmed(*this, &CCPACSWing::BuildFusedSegmentsUntrimmed) + , wingCleanShapeTrimmed(*this, &CCPACSWing::BuildFusedSegmentsTrimmed) , rebuildFusedSegWEdge(true) , rebuildShells(true) , buildFlaps(false) @@ -151,7 +153,8 @@ void CCPACSWing::InvalidateImpl(const boost::optional& source) cons loft.clear(); guideCurves.clear(); - wingCleanShape.clear(); + wingCleanShapeTrimmed.clear(); + wingCleanShapeUntrimmed.clear(); wingShapeWithCutouts.clear(); wingHelper.clear(); @@ -345,7 +348,7 @@ PNamedShape CCPACSWing::GetMirroredLoft(PNamedShape input_shape) const TopoDS_Shape& CCPACSWing::GetLoftWithLeadingEdge() { if (rebuildFusedSegWEdge) { - fusedSegmentWithEdge = (*wingCleanShape)->Shape(); + fusedSegmentWithEdge = (*wingCleanShapeUntrimmed)->Shape(); } rebuildFusedSegWEdge = false; return fusedSegmentWithEdge; @@ -399,10 +402,10 @@ PNamedShape CCPACSWing::BuildLoft() const else { if (GetConfiguration().HasDucts()) { - return GetConfiguration().GetDucts()->LoftWithDuctCutouts(*wingCleanShape, GetUID()); + return GetConfiguration().GetDucts()->LoftWithDuctCutouts(*wingCleanShapeUntrimmed, GetUID()); } - return *wingCleanShape; + return *wingCleanShapeUntrimmed; } return ret; @@ -412,17 +415,23 @@ TopoDS_Shape CCPACSWing::GetLoftWithCutouts() { if (NumberOfControlSurfaces(*this) == 0) { LOG(WARNING) << "No control devices defined, GetLoftWithCutOuts() will return a clean shape."; - return (*wingCleanShape)->Shape(); + return (*wingCleanShapeUntrimmed)->Shape(); } else { return (*wingShapeWithCutouts)->Shape(); } } -// Builds a fused shape of all wing segments -void CCPACSWing::BuildFusedSegments(PNamedShape& shape) const +// Builds a fused shape of all wing segments (trimmed, with profile cuts) +void CCPACSWing::BuildFusedSegmentsTrimmed(PNamedShape& shape) const +{ + shape = CTiglWingBuilder(*this, true); +} + +// Builds a fused shape of all wing segments (untrimmed, without profile cuts) +void CCPACSWing::BuildFusedSegmentsUntrimmed(PNamedShape& shape) const { - shape = CTiglWingBuilder(*this); + shape = CTiglWingBuilder(*this, false); } // Builds a fused shape of all wing segments @@ -485,7 +494,7 @@ void CCPACSWing::BuildWingWithCutouts(PNamedShape& result) const // BRepAlgoAPI pattern, instead of fusing the n cutouts pairwise into one // complex tool and then cutting once. TopTools_ListOfShape objects, tools; - objects.Append((*wingCleanShape)->Shape()); + objects.Append((*wingCleanShapeUntrimmed)->Shape()); for (const auto& cutoutShape : cutoutShapes) { tools.Append(cutoutShape->Shape()); } @@ -498,8 +507,8 @@ void CCPACSWing::BuildWingWithCutouts(PNamedShape& result) const throw CTiglError("Error cutting control surfaces from wing '" + GetUID() + "'"); } - PNamedShape cutCompound(new CNamedShape(cutter.Shape(), (*wingCleanShape)->Name())); - CBooleanOperTools::MapFaceNamesAfterBOP(cutter, *wingCleanShape, cutCompound); + PNamedShape cutCompound(new CNamedShape(cutter.Shape(), (*wingCleanShapeUntrimmed)->Name())); + CBooleanOperTools::MapFaceNamesAfterBOP(cutter, *wingCleanShapeUntrimmed, cutCompound); for (const auto& cutoutShape : cutoutShapes) { CBooleanOperTools::MapFaceNamesAfterBOP(cutter, cutoutShape, cutCompound); } @@ -515,12 +524,12 @@ void CCPACSWing::BuildWingWithCutouts(PNamedShape& result) const solidMaker.Add(TopoDS::Shell(shellMap(ishell))); } - result = PNamedShape(new CNamedShape(solidMaker.Solid(), (*wingCleanShape)->Name())); + result = PNamedShape(new CNamedShape(solidMaker.Solid(), (*wingCleanShapeUntrimmed)->Name())); CBooleanOperTools::MapFaceNamesAfterBOP(solidMaker, cutCompound, result); for (int iFace = 0; iFace < static_cast(result->GetFaceCount()); ++iFace) { CFaceTraits ft = result->GetFaceTraits(iFace); - ft.SetOrigin(*wingCleanShape); + ft.SetOrigin(*wingCleanShapeUntrimmed); result->SetFaceTraits(iFace, ft); } @@ -1799,7 +1808,26 @@ void CCPACSWing::SetBuildFlaps(bool build) PNamedShape CCPACSWing::GetWingCleanShape() const { - return *wingCleanShape; + return *wingCleanShapeUntrimmed; +} + +PNamedShape CCPACSWing::GetTrimmedWingCleanShape() const +{ + return *wingCleanShapeTrimmed; +} + +PNamedShape CCPACSWing::GetUntrimmedLoft() const +{ + // Note: unlike other components, the wing's GetLoft() folds in flaps and + // duct cutouts, so it is not suitable here. We return the raw untrimmed + // clean shape, parallel to GetTrimmedLoft() which returns the raw trimmed + // clean shape. + return *wingCleanShapeUntrimmed; +} + +PNamedShape CCPACSWing::GetTrimmedLoft() const +{ + return *wingCleanShapeTrimmed; } namespace diff --git a/src/wing/CCPACSWing.h b/src/wing/CCPACSWing.h index ad4421da62..ffde9fa3f2 100644 --- a/src/wing/CCPACSWing.h +++ b/src/wing/CCPACSWing.h @@ -189,6 +189,20 @@ friend class CTiglWingBuilder; */ TIGL_EXPORT CTiglTransformation GetPositioningTransformation(std::string sectionUID); + /** + * @brief Returns the wing loft (untrimmed, i.e. without UV cuts at profile positions). + * This is the default loft returned by GetLoft(). + * @return PNamedShape + */ + TIGL_EXPORT PNamedShape GetUntrimmedLoft() const; + + /** + * @brief Returns the wing loft with UV cuts at profile positions. + * This is the legacy trimmed behavior. + * @return PNamedShape + */ + TIGL_EXPORT PNamedShape GetTrimmedLoft() const override; + /** * @brief Returns the upper point in absolute (world) coordinates for a given segment, * eta, xsi (calculated output may be influenced by setting different value for Enum getPointBehavior) @@ -387,11 +401,17 @@ friend class CTiglWingBuilder; TIGL_EXPORT void SetBuildFlaps(bool enabled); /** - * @brief Returns the wing shape without flaps cut out + * @brief Returns the wing shape without flaps cut out (untrimmed, i.e. without UV cuts at profiles) * @return PNamedShape */ TIGL_EXPORT PNamedShape GetWingCleanShape() const; + /** + * @brief Returns the trimmed wing shape (with UV cuts at profile positions) + * @return PNamedShape + */ + TIGL_EXPORT PNamedShape GetTrimmedWingCleanShape() const; + TiglGetPointBehavior getPointBehavior {asParameterOnSurface}; /**< sets behavior of the GetPoint-function (default: asParameterOnSurface) */ // CREATOR FUNCTIONS @@ -607,8 +627,11 @@ friend class CTiglWingBuilder; // Update internal wing data void Update(); - // Adds all Segments of this wing to one shape - void BuildFusedSegments(PNamedShape& ) const; + // Adds all Segments of this wing to one shape (trimmed, with profile cuts) + void BuildFusedSegmentsTrimmed(PNamedShape& ) const; + + // Adds all Segments of this wing to one shape (untrimmed, without profile cuts) + void BuildFusedSegmentsUntrimmed(PNamedShape& ) const; PNamedShape BuildLoft() const override; @@ -641,7 +664,8 @@ friend class CTiglWingBuilder; Cache guideCurves; Cache wingShapeWithCutouts; /**< Wing without flaps / flaps removed */ - Cache wingCleanShape; /**< Clean wing surface without flaps cutout*/ + Cache wingCleanShapeUntrimmed; /**< Clean wing surface, untrimmed (without UV cuts at profiles) */ + Cache wingCleanShapeTrimmed; /**< Clean wing surface, trimmed (with UV cuts at profiles) */ mutable bool rebuildFusedSegWEdge; /**< Indicates if segmentation fusing need rebuild */ mutable bool rebuildShells; bool buildFlaps; /**< Indicates if the wing's loft shall include flaps */ diff --git a/src/wing/CCPACSWingSegment.cpp b/src/wing/CCPACSWingSegment.cpp index be658ecf8b..71aff7b741 100644 --- a/src/wing/CCPACSWingSegment.cpp +++ b/src/wing/CCPACSWingSegment.cpp @@ -350,11 +350,11 @@ PNamedShape GetParentLoft(const CCPACSWingSegment& segment) { if (segment.GetParent()->IsParent()) { const CCPACSWing* wing = segment.GetParent()->GetParent(); - return wing->GetWingCleanShape(); + return wing->GetTrimmedLoft(); } - else if (segment.GetParent()->IsParent()) { + else if (segment.GetParent()->IsParent()) { const CCPACSEnginePylon* pylon = segment.GetParent()->GetParent(); - return pylon->GetLoft(); + return pylon->GetTrimmedLoft(); } else { throw CTiglError("Invalid parent type"); @@ -428,7 +428,7 @@ PNamedShape CCPACSWingSegment::BuildLoft() const TopExp::MapShapes(wingLoft->Shape(), TopAbs_FACE, faceMap); int nFaces = faceMap.Extent(); int nSegments = segments->GetSegmentCount(); - int nFacesPerSegment = (nFaces - 2)/nSegments; + int nFacesPerSegment = FacesPerSegment(nFaces - 2, nSegments); // determine index of segment to retrieve the correct subshapes of the wing // Here we explicitly require the subshapes to be ordered consistently @@ -436,7 +436,14 @@ PNamedShape CCPACSWingSegment::BuildLoft() const const CCPACSWingSegment& ws = segments->GetSegment(j); if (GetUID() == ws.GetUID()) { for(int i=0; i nFaces) { + LOG(ERROR) << "CCPACSWingSegment::BuildLoft: computed face index " << faceIndex + << " is out of range [1, " << nFaces << "] for segment \"" << GetUID() + << "\". The trimmed parent loft does not contain the expected number of faces."; + throw CTiglError("CCPACSWingSegment::BuildLoft: face index out of range for segment \"" + GetUID() + "\".", TIGL_ERROR); + } + BB.Add(loftShell, TopoDS::Face(faceMap(faceIndex))); // guides } break; } @@ -460,7 +467,7 @@ PNamedShape CCPACSWingSegment::BuildLoft() const if (GetGuideCurves()) { guideCurveParams = GetGuideCurves()->GetRelativeCircumferenceParameters(); } - CTiglWingBuilder::SetFaceTraits(guideCurveParams, GetUID(), loft, innerConnection.GetProfile().HasBluntTE()); + CTiglWingBuilder::SetFaceTraits(guideCurveParams, GetUID(), loft, innerConnection.GetProfile().HasBluntTE(), false); return loft; } diff --git a/src/wing/CTiglWingBuilder.cpp b/src/wing/CTiglWingBuilder.cpp index d3b8090e37..f57c924b17 100644 --- a/src/wing/CTiglWingBuilder.cpp +++ b/src/wing/CTiglWingBuilder.cpp @@ -45,8 +45,9 @@ namespace tigl { -CTiglWingBuilder::CTiglWingBuilder(const CCPACSWing& wing) +CTiglWingBuilder::CTiglWingBuilder(const CCPACSWing& wing, bool enableProfileCutting) : _wing(wing) + , _enableProfileCutting(enableProfileCutting) { } @@ -61,6 +62,7 @@ PNamedShape CTiglWingBuilder::BuildShape() CTiglMakeLoft lofter; lofter.setMakeSolid(true); + lofter.setEnableProfileCutting(_enableProfileCutting); for (int i=1; i <= segments.GetSegmentCount(); i++) { const TopoDS_Shape& startWire = segments.GetSegment(i).GetInnerWire(); @@ -83,7 +85,7 @@ PNamedShape CTiglWingBuilder::BuildShape() std::string loftName = _wing.GetUID(); std::string loftShortName = _wing.GetShortShapeName(); PNamedShape loft(new CNamedShape(loftShape, loftName.c_str(), loftShortName.c_str())); - SetFaceTraits(_wing.GetGuideCurveStartParameters(), _wing.GetUID(), loft, hasBluntTE); + SetFaceTraits(_wing.GetGuideCurveStartParameters(), _wing.GetUID(), loft, hasBluntTE, _enableProfileCutting); return loft; } @@ -93,7 +95,7 @@ CTiglWingBuilder::operator PNamedShape() return BuildShape(); } // Set the name of each wing face -void CTiglWingBuilder::SetFaceTraits (const std::vector& guideCurveParams, const std::string& shapeUID, PNamedShape shape, bool hasBluntTE) +void CTiglWingBuilder::SetFaceTraits (const std::vector& guideCurveParams, const std::string& shapeUID, PNamedShape shape, bool hasBluntTE, bool enableProfileCutting) { auto params = guideCurveParams; assert(std::is_sorted(std::begin(params), std::end(params))); @@ -151,14 +153,19 @@ void CTiglWingBuilder::SetFaceTraits (const std::vector& guideCurveParam } if ((nFaces - 2) % nFacesPerSegment != 0) { - LOG(ERROR) << "CCPACSWingBuilder: Unable to determine wing face names from wing loft."; - return; + if (enableProfileCutting) { + LOG(WARNING) << "CCPACSWingBuilder: Face count mismatch in profile-cut loft. Expected (nFaces-2) to be divisible by " << nFacesPerSegment << ", got " << (nFaces-2) << ". Profile cutting may have altered face structure. Proceeding with sequential naming."; + } else { + LOG(WARNING) << "CCPACSWingBuilder: Unable to determine wing face names from wing loft."; + } } // assign "Top" and "Bottom" to face traits for (unsigned int i = 0; i < nFaces-2; i++) { CFaceTraits traits = shape->GetFaceTraits(i); - traits.SetName(names[i%names.size()]); + if (!names.empty()) { + traits.SetName(names[i%names.size()]); + } shape->SetFaceTraits(i, traits); } diff --git a/src/wing/CTiglWingBuilder.h b/src/wing/CTiglWingBuilder.h index c282dd6534..1d55e8c04c 100644 --- a/src/wing/CTiglWingBuilder.h +++ b/src/wing/CTiglWingBuilder.h @@ -28,17 +28,18 @@ namespace tigl class CTiglWingBuilder { public: - CTiglWingBuilder(const CCPACSWing& wing); + CTiglWingBuilder(const CCPACSWing& wing, bool enableProfileCutting = false); operator PNamedShape(); PNamedShape BuildShape(); - static void SetFaceTraits (const std::vector& guideCurveParams, const std::string& shapeUid, PNamedShape shape, bool hasBluntTE); + static void SetFaceTraits (const std::vector& guideCurveParams, const std::string& shapeUid, PNamedShape shape, bool hasBluntTE, bool enableProfileCutting = false); private: const CCPACSWing& _wing; + bool _enableProfileCutting; }; } //namespace tigl diff --git a/tests/unittests/testDuct.cpp b/tests/unittests/testDuct.cpp index 2069dafcd1..161f8799c6 100644 --- a/tests/unittests/testDuct.cpp +++ b/tests/unittests/testDuct.cpp @@ -129,7 +129,7 @@ TEST_F(DuctSimple, DuctLevel) } // Check the position of a sample duct with help of its bounding box - auto loftSimpleDuct = ductSimpleDuct->GetLoft(); + auto loftSimpleDuct = ductSimpleDuct->GetTrimmedLoft(); const TopoDS_Shape& shapeSimpleDuct = loftSimpleDuct->Shape(); Bnd_Box ductBBox; @@ -393,3 +393,16 @@ TEST_F(DuctSimple, tiglConfigurationGetWithDuctCutouts) EXPECT_TRUE(tigl::CCPACSConfigurationManager::GetInstance().GetConfiguration(DuctSimple::tiglHandle).GetDucts()->IsEnabled()); EXPECT_TRUE(flag); } + +TEST_F(DuctSimple, UntrimmedLoftAccessors) +{ + // Verify GetUntrimmedLoft() works and equals GetLoft() for ducts (no trimming path exists) + auto duct = ductSimpleDuct; + auto loft = duct->GetLoft(); + auto untrimmed = duct->GetUntrimmedLoft(); + + EXPECT_TRUE(loft != nullptr); + EXPECT_TRUE(untrimmed != nullptr); + + EXPECT_EQ(GetNumberOfFaces(loft->Shape()), GetNumberOfFaces(untrimmed->Shape())); +} diff --git a/tests/unittests/testFuselageStandardProfileSuperellipse.cpp b/tests/unittests/testFuselageStandardProfileSuperellipse.cpp index dda8b99329..609226d1d1 100644 --- a/tests/unittests/testFuselageStandardProfileSuperellipse.cpp +++ b/tests/unittests/testFuselageStandardProfileSuperellipse.cpp @@ -135,6 +135,53 @@ TEST_F(FuselageStandardProfileSuperEllipse, BuildFuselageMixedProfilesWithKinks_ ASSERT_TRUE(BRepCheck_Analyzer(fuselage->Shape()).IsValid()); } +TEST_F(FuselageStandardProfileSuperEllipse, UntrimmedLoftFaceNames_MultipleAeroFaces) +{ + // Regression test for the untrimmed fuselage face naming. + // + // The guides model produces an untrimmed loft with several aerodynamic faces + // (one per guide-curve sector) followed by the Front/Rear cap faces. The old + // untrimmed face-naming assumed a single aero face and cycled the cap names + // (loftName, "symmetry", "Front", "Rear") over every remaining face, which + // mislabeled aero faces as Front/Rear/symmetry and gave the caps wrong names. + // + // Buggy output was: SimpleFuselage, Front, Rear, SimpleFuselage, symmetry, + // Front, Rear, SimpleFuselage (3 aero / 1 symmetry / 2 Front / 2 Rear). + // Correct layout is [aero...][symmetry?][Front][Rear]: 6 aero, 0 symmetry, + // 1 Front, 1 Rear. + tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); + tigl::CCPACSConfiguration& config = manager.GetConfiguration(tiglHandle); + + tigl::CCPACSFuselage& fuselage = config.GetFuselage(1); + PNamedShape loft = fuselage.GetUntrimmedLoft(); + ASSERT_TRUE(loft != nullptr); + + const std::string aeroName = loft->Name(); + + int nAero = 0, nSymmetry = 0, nFront = 0, nRear = 0; + for (int i = 0; i < loft->GetFaceCount(); ++i) { + const std::string name = loft->GetFaceTraits(i).Name(); + if (name == "Front") { + ++nFront; + } + else if (name == "Rear") { + ++nRear; + } + else if (name == "symmetry") { + ++nSymmetry; + } + else if (name == aeroName) { + ++nAero; + } + } + + EXPECT_EQ(nAero, 6); + EXPECT_EQ(nSymmetry, 0); + EXPECT_EQ(nFront, 1); + EXPECT_EQ(nRear, 1); + EXPECT_EQ(loft->GetFaceCount(), 8); +} + TEST_F(FuselageStandardProfileSuperEllipse, BuildFuselageMixedProfilesInvalidInput) { tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); @@ -172,7 +219,7 @@ TEST(FuselageStandardProfileSuperEllipse_kinks, issue_1094) // check number of faces. It should be Front, Rear and additionally four faces, one face per quadrant. // If there are additional kinks, there are more faces - auto fuselage = config.GetFuselage(1).GetLoft(); + auto fuselage = config.GetFuselage(1).GetTrimmedLoft(); int face_count = 0; for (int i=0; i < fuselage->GetFaceCount(); ++i) { if (fuselage->GetFaceTraits(i).Name() != "Front" && fuselage->GetFaceTraits(i).Name() != "Rear") { diff --git a/tests/unittests/tiglCommonFunctions.cpp b/tests/unittests/tiglCommonFunctions.cpp index 68372d546d..aa7f34357c 100644 --- a/tests/unittests/tiglCommonFunctions.cpp +++ b/tests/unittests/tiglCommonFunctions.cpp @@ -505,4 +505,32 @@ TEST(TiglCommonFunctions, TiglAxisToCTiglPoint ) EXPECT_TRUE(TiglAxisToCTiglPoint(TIGL_X_AXIS) == tigl::CTiglPoint(1,0,0)); EXPECT_TRUE(TiglAxisToCTiglPoint(TIGL_Y_AXIS) == tigl::CTiglPoint(0,1,0)); EXPECT_TRUE(TiglAxisToCTiglPoint(TIGL_Z_AXIS) == tigl::CTiglPoint(0,0,1)); +} + +TEST(TiglCommonFunctions, FacesPerSegment) +{ + // Normal exact division + EXPECT_EQ(FacesPerSegment(6, 3), 2); + + // Ceil (non-divisible) + EXPECT_EQ(FacesPerSegment(7, 3), 3); + EXPECT_EQ(FacesPerSegment(4, 3), 2); + EXPECT_EQ(FacesPerSegment(5, 2), 3); + EXPECT_EQ(FacesPerSegment(10, 4), 3); + + // nSegments <= 0 guard + EXPECT_EQ(FacesPerSegment(6, 0), 1); + EXPECT_EQ(FacesPerSegment(6, -1), 1); + EXPECT_EQ(FacesPerSegment(6, -5), 1); + + // nFaces == 0 clamp + EXPECT_EQ(FacesPerSegment(0, 3), 1); + + // nFaces negative clamp + EXPECT_EQ(FacesPerSegment(-5, 3), 1); + + // Single segment + EXPECT_EQ(FacesPerSegment(5, 1), 5); + EXPECT_EQ(FacesPerSegment(1, 1), 1); + EXPECT_EQ(FacesPerSegment(100, 1), 100); } \ No newline at end of file diff --git a/tests/unittests/tiglControlSurfaceDevice.cpp b/tests/unittests/tiglControlSurfaceDevice.cpp index eee2c12df7..0542b330a4 100644 --- a/tests/unittests/tiglControlSurfaceDevice.cpp +++ b/tests/unittests/tiglControlSurfaceDevice.cpp @@ -403,3 +403,46 @@ TEST_F(TiglControlSurfaceDeviceSimple, bug_780_reference_segment) EXPECT_NEAR(min.Y(), 1.25, 1e-2); EXPECT_NEAR(max.Y(), 1.75, 1e-2); } + +// Regression test: leading edge device flap geometry must not be empty. +// +// The flap and cutout geometry of control surface devices is constructed via +// boolean operations against the wing loft. When the devices were switched to +// build against the untrimmed wing clean shape, the boolean intersection used +// by leading edge devices returned an empty shape, so the leading edge device +// flap geometry silently disappeared (no error, no visual). Trailing edge +// devices were unaffected. The fix builds device geometry from the trimmed wing +// clean shape (GetTrimmedWingCleanShape), which has robust face topology. +TEST_F(TiglControlSurfaceDeviceSimple, leadingEdgeDeviceFlapShapeNotEmpty) +{ + auto& manager = tigl::CCPACSConfigurationManager::GetInstance(); + auto& config = manager.GetConfiguration(tiglHandle); + auto& wing = config.GetWing(1); + auto& componentSegment = static_cast(wing.GetComponentSegment(1)); + auto& cs = *componentSegment.GetControlSurfaces(); + + auto countFaces = [](const TopoDS_Shape& s) { + int n = 0; + for (TopExp_Explorer e(s, TopAbs_FACE); e.More(); e.Next()) { + ++n; + } + return n; + }; + + // Trailing edge device flaps must have geometry (regression guard). + auto& teds = *cs.GetTrailingEdgeDevices(); + ASSERT_EQ(teds.GetTrailingEdgeDeviceCount(), 2); + for (int i = 1; i <= teds.GetTrailingEdgeDeviceCount(); ++i) { + auto flap = teds.GetTrailingEdgeDevice(i).GetFlapShape()->Shape(); + EXPECT_FALSE(flap.IsNull()); + EXPECT_GT(countFaces(flap), 0) << "trailing edge device flap shape is empty"; + } + + // Leading edge device flap must have geometry. Before the fix this was an + // empty compound (0 faces). + auto& leds = *cs.GetLeadingEdgeDevices(); + ASSERT_EQ(leds.GetLeadingEdgeDeviceCount(), 1); + auto ledFlap = leds.GetLeadingEdgeDevice(1).GetFlapShape()->Shape(); + EXPECT_FALSE(ledFlap.IsNull()); + EXPECT_GT(countFaces(ledFlap), 0) << "leading edge device flap shape is empty (regression)"; +} diff --git a/tests/unittests/tiglExports.cpp b/tests/unittests/tiglExports.cpp index 0716c8aea7..d3fc3a4894 100644 --- a/tests/unittests/tiglExports.cpp +++ b/tests/unittests/tiglExports.cpp @@ -493,8 +493,8 @@ TEST_F(tiglExportSymmetricWing, duplicateFaceBug) } // expected number of faces = 24 - // main wing: three segments with upper and lower face + wing tip = 7, symmetry -> 14 - // HTP: one segment with upper and lower face + wing tip = 3, symmetry -> 6 - // VTP: one segment with upper and lower face + wing tip + wing root = 4, no symmetry -> 4 - ASSERT_EQ(24, nfaces); + // main wing: upper and lower face + wing tip = 3, symmetry -> 6 + // HTP: upper and lower face + wing tip = 3, symmetry -> 6 + // VTP: upper and lower face + wing tip + wing root = 4, no symmetry -> 4 + ASSERT_EQ(16, nfaces); } diff --git a/tests/unittests/tiglGetCrossSectionArea.cpp b/tests/unittests/tiglGetCrossSectionArea.cpp index c134dd7626..25f12f7699 100644 --- a/tests/unittests/tiglGetCrossSectionArea.cpp +++ b/tests/unittests/tiglGetCrossSectionArea.cpp @@ -117,7 +117,11 @@ TEST_F(GetCrossSectionAreaSimple, area_computations_fused_airplane) EXPECT_EQ(tiglGetCrossSectionArea(tiglHandle, "Cpacs2Test", 0., 0., 0., 0., 0., 1., &area), TIGL_SUCCESS); - double precision = 1.E-5; + // The cross section is computed from the untrimmed loft (see tiglGetCrossSectionArea). + // OpenCASCADE approximates the boundary of the section (a b-spline) and slightly cuts off + // sharp kinks at the profiles, introducing a small error (measured ~1.8e-3 for this case). + // A tolerance of 1e-2 leaves headroom for OCCT-version / platform variation. + double precision = 1.E-2; ASSERT_NEAR(area, 4.5, precision); @@ -180,7 +184,11 @@ TEST_F(GetCrossSectionAreaSimple, area_computations_wing) EXPECT_EQ(tiglGetCrossSectionArea(tiglHandle, "Wing", 0., 0., 0., 0., 0., 1., &area), TIGL_SUCCESS); - double precision = 1.E-5; + // The cross section is computed from the untrimmed loft (see tiglGetCrossSectionArea). + // OpenCASCADE approximates the boundary of the section (a b-spline) and slightly cuts off + // sharp kinks at the profiles, introducing a small error (measured ~2.7e-3 for this case). + // A tolerance of 1e-2 leaves headroom for OCCT-version / platform variation. + double precision = 1.E-2; ASSERT_NEAR(area, 1.75, precision); diff --git a/tests/unittests/tiglLoftTrimming.cpp b/tests/unittests/tiglLoftTrimming.cpp new file mode 100644 index 0000000000..3e67d93a63 --- /dev/null +++ b/tests/unittests/tiglLoftTrimming.cpp @@ -0,0 +1,388 @@ +/* +* Copyright (C) 2026 German Aerospace Center +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +/** +* @file +* @brief Tests for trimmed vs untrimmed loft disambiguation (PR #1331). +* +* Verifies that GetLoft() defaults to GetUntrimmedLoft(), that trimming +* genuinely changes face count when guide curves are absent, and that +* trimming preserves geometry (bounding box, area, volume). +* Also verifies that guide-curve lofts ignore the trimming flag. +*/ + +#include "BRepBuilderAPI_Transform.hxx" +#include "BRepCheck_Analyzer.hxx" +#include "BRepGProp.hxx" +#include "BRepBndLib.hxx" +#include "Bnd_Box.hxx" +#include "CTiglError.h" +#include "CCPACSFuselage.h" +#include "CCPACSWing.h" +#include "CCPACSDuct.h" +#include "CCPACSEnginePylon.h" +#include "CCPACSConfigurationManager.h" +#include "CNamedShape.h" +#include "tiglcommonfunctions.h" +#include "test.h" +#include "testUtils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int countFaces(PNamedShape shape) +{ + if (!shape) { + return 0; + } + int count = 0; + TopExp_Explorer explorer(shape->Shape(), TopAbs_FACE); + while (explorer.More()) { + ++count; + explorer.Next(); + } + return count; +} + +double surfaceArea(PNamedShape shape) +{ + if (!shape) { + return 0.0; + } + GProp_GProps props; + BRepGProp::SurfaceProperties(shape->Shape(), props); + return props.Mass(); +} + +double volume(PNamedShape shape) +{ + if (!shape) { + return 0.0; + } + GProp_GProps props; + BRepGProp::VolumeProperties(shape->Shape(), props); + return props.Mass(); +} + +bool isClosedSolid(PNamedShape shape) +{ + if (!shape) { + return false; + } + return BRepCheck_Analyzer(shape->Shape()).IsValid(); +} + +double bboxSize(const Bnd_Box& box) +{ + if (box.IsVoid()) { + return 0.0; + } + return (box.CornerMax().X() - box.CornerMin().X()) * + (box.CornerMax().Y() - box.CornerMin().Y()) * + (box.CornerMax().Z() - box.CornerMin().Z()); +} + +double bboxDiagonal(const Bnd_Box& box) +{ + if (box.IsVoid()) { + return 0.0; + } + return sqrt( + pow(box.CornerMax().X() - box.CornerMin().X(), 2) + + pow(box.CornerMax().Y() - box.CornerMin().Y(), 2) + + pow(box.CornerMax().Z() - box.CornerMin().Z(), 2) + ); +} + +void expectNear(double a, double b, double relTol, const std::string& msg) +{ + double maxAbs = std::max(std::abs(a), std::abs(b)); + double diff = std::abs(a - b); + double tol = relTol * (maxAbs > 0 ? maxAbs : 1.0); + EXPECT_NEAR(a, b, tol) << msg << " (abs diff=" << diff << ", rel tol=" << relTol << ")"; +} + +class LoftTrimming : public ::testing::Test +{ +protected: + static void SetUpTestCase() + { + const char* filename = "TestData/simpletest.cpacs.xml"; + ReturnCode tixiRet; + TiglReturnCode tiglRet; + + tiglHandle = -1; + tixiHandle = -1; + + tixiRet = tixiOpenDocument(filename, &tixiHandle); + ASSERT_TRUE(tixiRet == SUCCESS); + tiglRet = tiglOpenCPACSConfiguration(tixiHandle, "Cpacs2Test", &tiglHandle); + ASSERT_TRUE(tiglRet == TIGL_SUCCESS); + } + + static void TearDownTestCase() + { + ASSERT_TRUE(tiglCloseCPACSConfiguration(tiglHandle) == TIGL_SUCCESS); + ASSERT_TRUE(tixiCloseDocument(tixiHandle) == SUCCESS); + tiglHandle = -1; + tixiHandle = -1; + } + + void SetUp() override {} + void TearDown() override {} + + static TixiDocumentHandle tixiHandle; + static TiglCPACSConfigurationHandle tiglHandle; +}; + +TixiDocumentHandle LoftTrimming::tixiHandle = 0; +TiglCPACSConfigurationHandle LoftTrimming::tiglHandle = 0; + +TEST_F(LoftTrimming, GetLoftDefaultsToUntrimmed) +{ + tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); + tigl::CCPACSConfiguration& config = manager.GetConfiguration(tiglHandle); + + // Wing + { + auto& wing = config.GetWing(1); + auto loft = wing.GetLoft(); + auto untrimmed = wing.GetUntrimmedLoft(); + EXPECT_EQ(countFaces(loft), countFaces(untrimmed)); + EXPECT_EQ(surfaceArea(loft), surfaceArea(untrimmed)); + EXPECT_EQ(volume(loft), volume(untrimmed)); + } + + // Fuselage + { + auto& fuselage = config.GetFuselage(1); + auto loft = fuselage.GetLoft(); + auto untrimmed = fuselage.GetUntrimmedLoft(); + EXPECT_EQ(countFaces(loft), countFaces(untrimmed)); + EXPECT_EQ(surfaceArea(loft), surfaceArea(untrimmed)); + EXPECT_EQ(volume(loft), volume(untrimmed)); + } +} + +TEST_F(LoftTrimming, TrimmedHasMoreFacesThanUntrimmed_NoGuides) +{ + tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); + tigl::CCPACSConfiguration& config = manager.GetConfiguration(tiglHandle); + + // Wing (no guide curves in simpletest) + { + auto& wing = config.GetWing(1); + int trimmedFaces = countFaces(wing.GetTrimmedLoft()); + int untrimmedFaces = countFaces(wing.GetUntrimmedLoft()); + EXPECT_GT(trimmedFaces, untrimmedFaces) << "Trimmed loft should have more faces than untrimmed when no guide curves"; + } + + // Fuselage (no guide curves in simpletest) + { + auto& fuselage = config.GetFuselage(1); + int trimmedFaces = countFaces(fuselage.GetTrimmedLoft()); + int untrimmedFaces = countFaces(fuselage.GetUntrimmedLoft()); + EXPECT_GT(trimmedFaces, untrimmedFaces) << "Trimmed loft should have more faces than untrimmed when no guide curves"; + } +} + +TEST_F(LoftTrimming, TrimmingPreservesGeometry) +{ + tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); + tigl::CCPACSConfiguration& config = manager.GetConfiguration(tiglHandle); + + double relTol = 1e-6; + + // Wing + { + const auto& wing = config.GetWing(1); + auto trimmed = wing.GetTrimmedLoft(); + auto untrimmed = wing.GetUntrimmedLoft(); + + EXPECT_TRUE(isClosedSolid(trimmed)); + EXPECT_TRUE(isClosedSolid(untrimmed)); + + Bnd_Box trimmedBox, untrimmedBox; + BRepBndLib::AddOptimal(trimmed->Shape(), trimmedBox); + BRepBndLib::AddOptimal(untrimmed->Shape(), untrimmedBox); + + double diag = bboxDiagonal(trimmedBox); + double lenTol = relTol * diag; + + EXPECT_NEAR(trimmedBox.CornerMin().X(), untrimmedBox.CornerMin().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Y(), untrimmedBox.CornerMin().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Z(), untrimmedBox.CornerMin().Z(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().X(), untrimmedBox.CornerMax().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Y(), untrimmedBox.CornerMax().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Z(), untrimmedBox.CornerMax().Z(), lenTol); + } + + // Fuselage + { + const auto& fuselage = config.GetFuselage(1); + auto trimmed = fuselage.GetTrimmedLoft(); + auto untrimmed = fuselage.GetUntrimmedLoft(); + + EXPECT_TRUE(isClosedSolid(trimmed)); + EXPECT_TRUE(isClosedSolid(untrimmed)); + + Bnd_Box trimmedBox, untrimmedBox; + BRepBndLib::AddOptimal(trimmed->Shape(), trimmedBox); + BRepBndLib::AddOptimal(untrimmed->Shape(), untrimmedBox); + + double diag = bboxDiagonal(trimmedBox); + double lenTol = 1e-6 * diag; + + EXPECT_NEAR(trimmedBox.CornerMin().X(), untrimmedBox.CornerMin().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Y(), untrimmedBox.CornerMin().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Z(), untrimmedBox.CornerMin().Z(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().X(), untrimmedBox.CornerMax().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Y(), untrimmedBox.CornerMax().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Z(), untrimmedBox.CornerMax().Z(), lenTol); + } +} + +TEST_F(LoftTrimming, WingTrimmedCleanShape) +{ + tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); + tigl::CCPACSConfiguration& config = manager.GetConfiguration(tiglHandle); + + const auto& wing = config.GetWing(1); + auto trimmedClean = wing.GetTrimmedWingCleanShape(); + auto untrimmedClean = wing.GetWingCleanShape(); + + EXPECT_TRUE(trimmedClean != nullptr); + EXPECT_TRUE(untrimmedClean != nullptr); +} + +class LoftTrimmingWithGuides : public ::testing::Test +{ +protected: + static void SetUpTestCase() + { + const char* filename = "TestData/simpletest-with-guides.cpacs.xml"; + ReturnCode tixiRet; + TiglReturnCode tiglRet; + + tiglHandle = -1; + tixiHandle = -1; + + tixiRet = tixiOpenDocument(filename, &tixiHandle); + ASSERT_TRUE(tixiRet == SUCCESS); + tiglRet = tiglOpenCPACSConfiguration(tixiHandle, "Cpacs2Test", &tiglHandle); + ASSERT_TRUE(tiglRet == TIGL_SUCCESS); + } + + static void TearDownTestCase() + { + ASSERT_TRUE(tiglCloseCPACSConfiguration(tiglHandle) == TIGL_SUCCESS); + ASSERT_TRUE(tixiCloseDocument(tixiHandle) == SUCCESS); + tiglHandle = -1; + tixiHandle = -1; + } + + void SetUp() override {} + void TearDown() override {} + + static TixiDocumentHandle tixiHandle; + static TiglCPACSConfigurationHandle tiglHandle; +}; + +TixiDocumentHandle LoftTrimmingWithGuides::tixiHandle = 0; +TiglCPACSConfigurationHandle LoftTrimmingWithGuides::tiglHandle = 0; + +TEST_F(LoftTrimmingWithGuides, GuideCurveLoftIgnoresTrimming) +{ + tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); + tigl::CCPACSConfiguration& config = manager.GetConfiguration(tiglHandle); + + // Wing (has guide curves) + { + const auto& wing = config.GetWing(1); + int trimmedFaces = countFaces(wing.GetTrimmedLoft()); + int untrimmedFaces = countFaces(wing.GetUntrimmedLoft()); + EXPECT_EQ(trimmedFaces, untrimmedFaces) << "Guide-curve loft should ignore trimming flag"; + } +} + +TEST_F(LoftTrimmingWithGuides, TrimmedVsUntrimmedPreservesGeometry_WithGuides) +{ + tigl::CCPACSConfigurationManager& manager = tigl::CCPACSConfigurationManager::GetInstance(); + tigl::CCPACSConfiguration& config = manager.GetConfiguration(tiglHandle); + + // Wing + { + const auto& wing = config.GetWing(1); + auto trimmed = wing.GetTrimmedLoft(); + auto untrimmed = wing.GetUntrimmedLoft(); + + EXPECT_TRUE(isClosedSolid(trimmed)); + EXPECT_TRUE(isClosedSolid(untrimmed)); + + Bnd_Box trimmedBox, untrimmedBox; + BRepBndLib::AddOptimal(trimmed->Shape(), trimmedBox); + BRepBndLib::AddOptimal(untrimmed->Shape(), untrimmedBox); + + double diag = bboxDiagonal(trimmedBox); + double lenTol = 1e-6 * diag; + + EXPECT_NEAR(trimmedBox.CornerMin().X(), untrimmedBox.CornerMin().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Y(), untrimmedBox.CornerMin().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Z(), untrimmedBox.CornerMin().Z(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().X(), untrimmedBox.CornerMax().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Y(), untrimmedBox.CornerMax().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Z(), untrimmedBox.CornerMax().Z(), lenTol); + } + + // Fuselage + { + const auto& fuselage = config.GetFuselage(1); + auto trimmed = fuselage.GetTrimmedLoft(); + auto untrimmed = fuselage.GetUntrimmedLoft(); + + EXPECT_TRUE(isClosedSolid(trimmed)); + EXPECT_TRUE(isClosedSolid(untrimmed)); + + Bnd_Box trimmedBox, untrimmedBox; + BRepBndLib::AddOptimal(trimmed->Shape(), trimmedBox); + BRepBndLib::AddOptimal(untrimmed->Shape(), untrimmedBox); + + double diag = bboxDiagonal(trimmedBox); + double lenTol = 1e-6 * diag; + + EXPECT_NEAR(trimmedBox.CornerMin().X(), untrimmedBox.CornerMin().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Y(), untrimmedBox.CornerMin().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMin().Z(), untrimmedBox.CornerMin().Z(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().X(), untrimmedBox.CornerMax().X(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Y(), untrimmedBox.CornerMax().Y(), lenTol); + EXPECT_NEAR(trimmedBox.CornerMax().Z(), untrimmedBox.CornerMax().Z(), lenTol); + } +} + +} // anonymous namespace diff --git a/tests/unittests/tiglSystems.cpp b/tests/unittests/tiglSystems.cpp index ac50fe6541..1b7861d5aa 100644 --- a/tests/unittests/tiglSystems.cpp +++ b/tests/unittests/tiglSystems.cpp @@ -264,7 +264,7 @@ TEST_F(Systems, ComponentsGeometry) const auto& multiSegment = GetComponent("wing"); PNamedShape shape = multiSegment.GetLoft(); ASSERT_TRUE(shape); - EXPECT_EQ(shape->GetFaceCount(), 4u); + EXPECT_EQ(shape->GetFaceCount(), 3u); Bnd_Box box; BRepBndLib::Add(shape->Shape(), box); @@ -277,11 +277,12 @@ TEST_F(Systems, ComponentsGeometry) } // multiSegmentShape with 2 segments and super ellipses + // Note: multiSegmentShapes geometry doesn't support trimming, so GetTrimmedLoft() == GetUntrimmedLoft() == GetLoft() { const auto& multiSegment = GetComponent("multiSegmentComponent3"); PNamedShape shape = multiSegment.GetLoft(); ASSERT_TRUE(shape); - EXPECT_EQ(shape->GetFaceCount(), 10u); + EXPECT_EQ(shape->GetFaceCount(), 3u); } } diff --git a/tests/unittests/tiglTanks.cpp b/tests/unittests/tiglTanks.cpp index 990e7ecca5..a7f15b0d73 100644 --- a/tests/unittests/tiglTanks.cpp +++ b/tests/unittests/tiglTanks.cpp @@ -32,6 +32,10 @@ #include "CCPACSVessel.h" #include "CNamedShape.h" +#include +#include +#include + namespace { // Error message constants for exception tests @@ -39,6 +43,20 @@ constexpr const char* tankTypeExceptionString = "This method is only available for vessels with segments. No segment found."; constexpr const char* invalidIndexMessage = "Invalid index in CCPACSFuselageSections::GetSection"; constexpr const char* wrongSectionUIDMessage = "GetSectionFace: Could not find a fuselage section for the given UID"; + +int countFaces(PNamedShape shape) +{ + if (!shape) { + return 0; + } + int count = 0; + TopExp_Explorer explorer(shape->Shape(), TopAbs_FACE); + while (explorer.More()) { + ++count; + explorer.Next(); + } + return count; +} } // anonymous namespace // Dummy class for exception handling tests @@ -373,9 +391,8 @@ TEST_F(FuelTanks, vessel_face_traits) auto standard_loft = vessel_segments->GetLoft(); EXPECT_EQ(standard_loft->FaceTraits(0).Name(), vessel_segments->GetUID()); - EXPECT_EQ(standard_loft->FaceTraits(1).Name(), vessel_segments->GetUID()); - EXPECT_EQ(standard_loft->FaceTraits(2).Name(), "Front"); - EXPECT_EQ(standard_loft->FaceTraits(3).Name(), "Rear"); + EXPECT_EQ(standard_loft->FaceTraits(1).Name(), "Front"); + EXPECT_EQ(standard_loft->FaceTraits(2).Name(), "Rear"); auto parametric_loft = vessel_torispherical->GetLoft(); EXPECT_EQ(parametric_loft->FaceTraits(0).Name(), "Dome"); @@ -404,3 +421,14 @@ TEST_F(FuelTanks, structure) EXPECT_DOUBLE_EQ(p.Y(), -1); EXPECT_DOUBLE_EQ(p.Z(), -0.2); } + +TEST_F(FuelTanks, vessel_parametric_trimmed_fallback) +{ + // vessel_torispherical is a parametric vessel (no segments), so GetTrimmedLoft() should fall back to GetUntrimmedLoft() + auto tank4 = vessel_torispherical; + auto trimmed = tank4->GetTrimmedLoft(); + auto untrimmed = tank4->GetUntrimmedLoft(); + + EXPECT_TRUE(trimmed != nullptr); + EXPECT_EQ(countFaces(trimmed), countFaces(untrimmed)); +}