diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/CMakeLists.txt b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/CMakeLists.txt new file mode 100644 index 000000000000..a03de64ce903 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/CMakeLists.txt @@ -0,0 +1,3 @@ +project(SubdivisionQuadEdgeMeshFilter) + +itk_module_impl() diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/LICENSE b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/LICENSE new file mode 100644 index 000000000000..e5583c184e67 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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 + + https://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. diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/README.md b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/README.md new file mode 100644 index 000000000000..f910ca834a8b --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/README.md @@ -0,0 +1,29 @@ +# SubdivisionQuadEdgeMeshFilter + +In-tree ITK module providing triangle- and edge-based subdivision +filters for `itk::QuadEdgeMesh`. The filters refine a coarse triangular +quad-edge mesh into a finer one using classical subdivision schemes +(Loop, Modified Butterfly, Square Three, Linear), with both cell-based +and edge-based variants and a conditional/criterion-driven dispatcher +for adaptive refinement. + +The flagship classes are +`itk::SubdivisionQuadEdgeMeshFilter`, +`itk::TriangleCellSubdivisionQuadEdgeMeshFilter`, +`itk::TriangleEdgeCellSubdivisionQuadEdgeMeshFilter`, +`itk::ConditionalSubdivisionQuadEdgeMeshFilter`, and the concrete +`Loop`, `LinearTriangle`, `ModifiedButterfly`, and `SquareThree` +specializations. + +## Origin + +Ingested from the standalone remote module +[**InsightSoftwareConsortium/itkSubdivisionQuadEdgeMeshFilter**](https://github.com/InsightSoftwareConsortium/itkSubdivisionQuadEdgeMeshFilter) +on 2026-05-07 via the v4 ingestion pipeline. The upstream repository +will be archived read-only after this PR merges; it remains reachable +at the URL above for historical reference. + +## References + +- Zhu W. *Triangle Mesh Subdivision.* The Insight Journal. 2010. + diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkCellAreaTriangleCellSubdivisionCriterion.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkCellAreaTriangleCellSubdivisionCriterion.h new file mode 100644 index 000000000000..224247190d0b --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkCellAreaTriangleCellSubdivisionCriterion.h @@ -0,0 +1,86 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkCellAreaTriangleCellSubdivisionCriterion_h +#define itkCellAreaTriangleCellSubdivisionCriterion_h + +#include "itkQuadEdgeMeshSubdivisionCriterion.h" +#include "itkObjectFactory.h" +#include "itkTriangleHelper.h" +#include "itkNumericTraits.h" + + +namespace itk +{ +/** + *\class CellAreaTriangleCellSubdivisionCriterion + *\brief + *\ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class CellAreaTriangleCellSubdivisionCriterion : public QuadEdgeMeshSubdivisionCriterion +{ +public: + using Self = CellAreaTriangleCellSubdivisionCriterion; + using Superclass = QuadEdgeMeshSubdivisionCriterion; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using MeshType = typename Superclass::MeshType; + using MeshPointer = typename Superclass::MeshPointer; + using MeshConstPointer = typename Superclass::MeshConstPointer; + using PointsContainerPointer = typename Superclass::PointsContainerPointer; + using PointsContainerConstIterator = typename Superclass::PointsContainerConstIterator; + using PointsContainerIterator = typename Superclass::PointsContainerIterator; + using CellsContainer = typename Superclass::CellsContainer; + using CellsContainerPointer = typename Superclass::CellsContainerPointer; + using CellsContainerIterator = typename Superclass::CellsContainerIterator; + using CellsContainerConstIterator = typename Superclass::CellsContainerConstIterator; + using PointType = typename Superclass::PointType; + using CoordinateType = typename Superclass::CoordinateType; + using PointIdentifier = typename Superclass::PointIdentifier; + using CellIdentifier = typename Superclass::CellIdentifier; + using CellType = typename Superclass::CellType; + using QEType = typename Superclass::QEType; + using PointIdIterator = typename Superclass::PointIdIterator; + using SubdivisionCellContainer = typename Superclass::SubdivisionCellContainer; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(CellAreaTriangleCellSubdivisionCriterion); + itkNewMacro(Self); + + void + Compute(MeshType * mesh, SubdivisionCellContainer & cellIds) override; + + itkGetConstMacro(MaximumArea, CoordinateType); + itkSetMacro(MaximumArea, CoordinateType); + +protected: + CellAreaTriangleCellSubdivisionCriterion() { m_MaximumArea = NumericTraits::max(); } + ~CellAreaTriangleCellSubdivisionCriterion() override = default; + +private: + CoordinateType m_MaximumArea; +}; + +} // namespace itk +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkCellAreaTriangleCellSubdivisionCriterion.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkCellAreaTriangleCellSubdivisionCriterion.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkCellAreaTriangleCellSubdivisionCriterion.hxx new file mode 100644 index 000000000000..75578cfb9994 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkCellAreaTriangleCellSubdivisionCriterion.hxx @@ -0,0 +1,68 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkCellAreaTriangleCellSubdivisionCriterion_hxx +#define itkCellAreaTriangleCellSubdivisionCriterion_hxx + + +namespace itk +{ +template +void +CellAreaTriangleCellSubdivisionCriterion::Compute(MeshType * mesh, + SubdivisionCellContainer & cellIds) +{ + cellIds.clear(); + const CellsContainer * cells = mesh->GetCells(); + if (!cells) + { + itkExceptionMacro("<Begin(); + while (cter != cells->End()) + { + CellType * cell = cter->Value(); + if (!cell || cell->GetType() != CellGeometryEnum::POLYGON_CELL || cell->GetNumberOfPoints() != 3) + { + continue; + } + + PointType pointArray[3]; + PointIdIterator pter = cell->PointIdsBegin(); + PointIdentifier nn = 0; + + while (pter != cell->PointIdsEnd()) + { + mesh->GetPoint(*pter, &pointArray[nn]); + ++pter; + ++nn; + } + + CoordinateType area = TriangleHelper::ComputeArea(pointArray[0], pointArray[1], pointArray[2]); + if (area > m_MaximumArea) + { + cellIds.push_back(static_cast(cter->Index())); + } + + ++cter; + } +} + +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkConditionalSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkConditionalSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..cd277efe570f --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkConditionalSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,100 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkConditionalSubdivisionQuadEdgeMeshFilter_h +#define itkConditionalSubdivisionQuadEdgeMeshFilter_h + +#include "itkQuadEdgeMeshToQuadEdgeMeshFilter.h" +#include "itkQuadEdgeMeshSubdivisionCriterion.h" +#include "itkConceptChecking.h" + +namespace itk +{ +/** + * \class ConditionalSubdivisionQuadEdgeMeshFilter + * + * \brief FIXME + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class ConditionalSubdivisionQuadEdgeMeshFilter + : public QuadEdgeMeshToQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(ConditionalSubdivisionQuadEdgeMeshFilter); + + using Self = ConditionalSubdivisionQuadEdgeMeshFilter; + using Superclass = QuadEdgeMeshToQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using SubdivisionFilterType = TSubdivisionFilter; + using SubdivisionFilterPointer = typename SubdivisionFilterType::Pointer; + + using InputMeshType = TInputMesh; + using InputMeshPointer = typename InputMeshType::Pointer; + + using OutputMeshType = typename SubdivisionFilterType::OutputMeshType; + using OutputMeshPointer = typename OutputMeshType::Pointer; + + using SubdivisionCellContainer = typename SubdivisionFilterType::SubdivisionCellContainer; + + using CriterionType = QuadEdgeMeshSubdivisionCriterion; + using CriterionPointer = typename CriterionType::Pointer; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(ConditionalSubdivisionQuadEdgeMeshFilter); + itkNewMacro(Self); + +#ifdef ITK_USE_CONCEPT_CHECKING + itkConceptMacro( + SameTypeCheck, + (Concept::SameType)); + itkConceptMacro( + SameTypeCheckMesh, + (Concept::SameType)); + itkConceptMacro(SameTypeCheckContainer, + (Concept::SameType)); +#endif + + void + SetSubdivisionCriterion(CriterionType * criterion); + +protected: + ConditionalSubdivisionQuadEdgeMeshFilter(); + + ~ConditionalSubdivisionQuadEdgeMeshFilter() override = default; + + void + GenerateData() override; + + void + PrintSelf(std::ostream & os, Indent indent) const override; + + SubdivisionFilterPointer m_SubdivisionFilter; + SubdivisionCellContainer m_CellsToBeSubdivided; + CriterionPointer m_SubdivisionCriterion; +}; +} // end namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkConditionalSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkConditionalSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkConditionalSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..5973af6e7fb0 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkConditionalSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,67 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkConditionalSubdivisionQuadEdgeMeshFilter_hxx +#define itkConditionalSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +ConditionalSubdivisionQuadEdgeMeshFilter::ConditionalSubdivisionQuadEdgeMeshFilter() +{ + this->m_SubdivisionFilter = SubdivisionFilterType::New(); +} + +template +void +ConditionalSubdivisionQuadEdgeMeshFilter::SetSubdivisionCriterion( + CriterionType * criterion) +{ + this->m_SubdivisionCriterion = criterion; + this->Modified(); +} + +template +void +ConditionalSubdivisionQuadEdgeMeshFilter::GenerateData() +{ + this->CopyInputMeshToOutputMeshGeometry(); + this->m_SubdivisionCriterion->Compute(this->GetOutput(), this->m_CellsToBeSubdivided); + + while (!this->m_CellsToBeSubdivided.empty()) + { + this->m_SubdivisionFilter->SetInput(this->GetOutput()); + this->m_SubdivisionFilter->SetCellsToBeSubdivided(this->m_CellsToBeSubdivided); + this->m_SubdivisionFilter->Update(); + OutputMeshPointer mesh = this->m_SubdivisionFilter->GetOutput(); + mesh->DisconnectPipeline(); + this->GraftOutput(mesh); + this->m_SubdivisionCriterion->Compute(this->GetOutput(), this->m_CellsToBeSubdivided); + } +} + +template +void +ConditionalSubdivisionQuadEdgeMeshFilter::PrintSelf(std::ostream & os, + Indent indent) const +{ + Superclass::PrintSelf(os, indent); +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.h new file mode 100644 index 000000000000..faa868cffc4e --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.h @@ -0,0 +1,85 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkEdgeLengthTriangleEdgeCellSubdivisionCriterion_h +#define itkEdgeLengthTriangleEdgeCellSubdivisionCriterion_h + +#include "itkQuadEdgeMeshSubdivisionCriterion.h" +#include "itkObjectFactory.h" +#include "itkNumericTraits.h" + + +namespace itk +{ +/** + *\class EdgeLengthTriangleEdgeCellSubdivisionCriterion + *\brief + *\ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class EdgeLengthTriangleEdgeCellSubdivisionCriterion : public QuadEdgeMeshSubdivisionCriterion +{ +public: + using Self = EdgeLengthTriangleEdgeCellSubdivisionCriterion; + using Superclass = QuadEdgeMeshSubdivisionCriterion; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using MeshType = typename Superclass::MeshType; + using MeshPointer = typename Superclass::MeshPointer; + using MeshConstPointer = typename Superclass::MeshConstPointer; + using PointsContainerPointer = typename Superclass::PointsContainerPointer; + using PointsContainerConstIterator = typename Superclass::PointsContainerConstIterator; + using PointsContainerIterator = typename Superclass::PointsContainerIterator; + using CellsContainer = typename Superclass::CellsContainer; + using CellsContainerPointer = typename Superclass::CellsContainerPointer; + using CellsContainerIterator = typename Superclass::CellsContainerIterator; + using CellsContainerConstIterator = typename Superclass::CellsContainerConstIterator; + using PointType = typename Superclass::PointType; + using CoordinateType = typename Superclass::CoordinateType; + using PointIdentifier = typename Superclass::PointIdentifier; + using CellIdentifier = typename Superclass::CellIdentifier; + using CellType = typename Superclass::CellType; + using QEType = typename Superclass::QEType; + using PointIdIterator = typename Superclass::PointIdIterator; + using SubdivisionCellContainer = typename Superclass::SubdivisionCellContainer; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(EdgeLengthTriangleEdgeCellSubdivisionCriterion); + itkNewMacro(Self); + + void + Compute(MeshType * mesh, SubdivisionCellContainer & edgeList) override; + + itkGetConstMacro(MaximumLength, CoordinateType); + itkSetMacro(MaximumLength, CoordinateType); + +protected: + EdgeLengthTriangleEdgeCellSubdivisionCriterion() { m_MaximumLength = NumericTraits::max(); } + ~EdgeLengthTriangleEdgeCellSubdivisionCriterion() override = default; + +private: + CoordinateType m_MaximumLength; +}; + +} // namespace itk +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.hxx new file mode 100644 index 000000000000..9f1adb5c6ca9 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.hxx @@ -0,0 +1,52 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkEdgeLengthTriangleEdgeCellSubdivisionCriterion_hxx +#define itkEdgeLengthTriangleEdgeCellSubdivisionCriterion_hxx + + +namespace itk +{ +template +void +EdgeLengthTriangleEdgeCellSubdivisionCriterion::Compute(MeshType * mesh, SubdivisionCellContainer & edgeList) +{ + edgeList.clear(); + typename MeshType::CellsContainer::ConstPointer edges = mesh->GetEdgeCells(); + if (!edges) + { + itkExceptionMacro("<Begin(); + while (eter != edges->End()) + { + auto * edge = dynamic_cast(eter.Value()); + if (edge) + { + if (mesh->ComputeEdgeLength(edge->GetQEGeom()) > m_MaximumLength) + { + edgeList.push_back(edge->GetQEGeom()); + } + } + ++eter; + } +} + +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..7c1b053299bc --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,101 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter_h +#define itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkQuadEdgeMeshToQuadEdgeMeshFilter.h" +#include "itkConceptChecking.h" +#include + +namespace itk +{ +/** + * \class IterativeTriangleCellSubdivisionQuadEdgeMeshFilter + * + * \brief FIXME + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class IterativeTriangleCellSubdivisionQuadEdgeMeshFilter + : public QuadEdgeMeshToQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(IterativeTriangleCellSubdivisionQuadEdgeMeshFilter); + + using Self = IterativeTriangleCellSubdivisionQuadEdgeMeshFilter; + using Superclass = QuadEdgeMeshToQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using CellSubdivisionFilterType = TCellSubdivisionFilter; + using CellSubdivisionFilterPointer = typename CellSubdivisionFilterType::Pointer; + using CellSubdivisionFilterPointerVector = typename std::vector; + + using InputMeshType = TInputMesh; + using InputMeshPointer = typename InputMeshType::Pointer; + + using OutputMeshType = typename CellSubdivisionFilterType::OutputMeshType; + using OutputMeshPointer = typename OutputMeshType::Pointer; + + using OutputCellIdentifier = typename CellSubdivisionFilterType::OutputCellIdentifier; + using SubdivisionCellContainer = typename CellSubdivisionFilterType::SubdivisionCellContainer; + using SubdivisionCellContainerConstIterator = + typename CellSubdivisionFilterType::SubdivisionCellContainerConstIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(IterativeTriangleCellSubdivisionQuadEdgeMeshFilter); + itkNewMacro(Self); + +#ifdef ITK_USE_CONCEPT_CHECKING + itkConceptMacro(SameTypeCheck, + (Concept::SameType)); +#endif + + itkSetMacro(ResolutionLevels, unsigned int); + itkGetConstMacro(ResolutionLevels, unsigned int); + void + SetCellsToBeSubdivided(const SubdivisionCellContainer &); + itkGetConstReferenceMacro(CellsToBeSubdivided, SubdivisionCellContainer); + + void + AddSubdividedCellId(OutputCellIdentifier cellId); + +protected: + IterativeTriangleCellSubdivisionQuadEdgeMeshFilter(); + + ~IterativeTriangleCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + GenerateData() override; + + void + PrintSelf(std::ostream & os, Indent indent) const override; + + CellSubdivisionFilterPointerVector m_CellSubdivisionFilterVector; + SubdivisionCellContainer m_CellsToBeSubdivided; + unsigned int m_ResolutionLevels; +}; +} // end namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..93f54bf9d190 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,84 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter_hxx + +#include + +namespace itk +{ +template +IterativeTriangleCellSubdivisionQuadEdgeMeshFilter:: + IterativeTriangleCellSubdivisionQuadEdgeMeshFilter() +{ + this->m_ResolutionLevels = 1; +} + +template +void +IterativeTriangleCellSubdivisionQuadEdgeMeshFilter::SetCellsToBeSubdivided( + const SubdivisionCellContainer & cellIdList) +{ + this->m_CellsToBeSubdivided = cellIdList; + this->Modified(); +} + +template +void +IterativeTriangleCellSubdivisionQuadEdgeMeshFilter::AddSubdividedCellId( + OutputCellIdentifier cellId) +{ + this->m_CellsToBeSubdivided.push_back(cellId); + this->Modified(); +} + +template +void +IterativeTriangleCellSubdivisionQuadEdgeMeshFilter::GenerateData() +{ + this->m_CellSubdivisionFilterVector.clear(); + const auto resolution = std::max(static_cast(1), this->m_ResolutionLevels); + for (size_t i = 0; i < resolution; ++i) + { + this->m_CellSubdivisionFilterVector.push_back(TCellSubdivisionFilter::New()); + } + this->m_CellSubdivisionFilterVector.at(0)->SetInput(this->GetInput()); + this->m_CellSubdivisionFilterVector.at(0)->SetCellsToBeSubdivided(this->m_CellsToBeSubdivided); + this->m_CellSubdivisionFilterVector.at(0)->Update(); + this->GraftOutput(this->m_CellSubdivisionFilterVector.at(0)->GetOutput()); + for (size_t i = 1; i < resolution; ++i) + { + this->m_CellSubdivisionFilterVector.at(i)->SetInput(this->GetOutput()); + this->m_CellSubdivisionFilterVector.at(i)->SetCellsToBeSubdivided( + this->m_CellSubdivisionFilterVector.at(i - 1)->GetCellsToBeSubdivided()); + this->m_CellSubdivisionFilterVector.at(i)->Update(); + this->GraftOutput(this->m_CellSubdivisionFilterVector.at(i)->GetOutput()); + } +} + +template +void +IterativeTriangleCellSubdivisionQuadEdgeMeshFilter::PrintSelf(std::ostream & os, + Indent indent) const +{ + Superclass::PrintSelf(os, indent); + std::cout << indent << "Subdivision Resolution Levels: " << m_ResolutionLevels << std::endl; +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..2906d7fcaddd --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,99 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter_h +#define itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ + +/** \class LinearTriangleCellSubdivisionQuadEdgeMeshFilter + * \brief It is the simplest interpolating subdivision scheme, the new vertices + * are defined as the middle point of each edge in a triangle. + * + * Each triangle is split into 4 new triangles. + * + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class LinearTriangleCellSubdivisionQuadEdgeMeshFilter + : public TriangleCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(LinearTriangleCellSubdivisionQuadEdgeMeshFilter); + + using Self = LinearTriangleCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputPointType = typename Superclass::InputPointType; + using InputVectorType = typename Superclass::InputVectorType; + using InputCoordType = typename Superclass::InputCoordType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputVectorType = typename Superclass::OutputVectorType; + using OutputCoordType = typename Superclass::OutputCoordType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + using EdgePointIdentifierContainer = typename Superclass::EdgePointIdentifierContainer; + using EdgePointIdentifierContainerPointer = typename Superclass::EdgePointIdentifierContainerPointer; + using EdgePointIdentifierContainerConstIterator = typename Superclass::EdgePointIdentifierContainerConstIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(LinearTriangleCellSubdivisionQuadEdgeMeshFilter); + + /** New macro for creation of through a Smart Pointer */ + itkNewMacro(Self); + +protected: + LinearTriangleCellSubdivisionQuadEdgeMeshFilter() = default; + ~LinearTriangleCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + AddNewCellPoints(InputCellType * cell) override; +}; +} // namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..81978aab0bcb --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,72 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ +#ifndef itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +void +LinearTriangleCellSubdivisionQuadEdgeMeshFilter::AddNewCellPoints(InputCellType * cell) +{ + if (cell->GetType() != CellGeometryEnum::POLYGON_CELL || cell->GetNumberOfPoints() != 3) + { + itkExceptionMacro(<< " The input cell is not a triangle cell"); + } + + OutputMeshType * output = this->GetOutput(); + + InputPointIdentifier pointIdArray[3]; + InputPointType pointArray[3]; + + InputPointIdIterator it = cell->PointIdsBegin(); + OutputPointIdentifier n = 0; + OutputPointIdentifier numberOfPoints = output->GetNumberOfPoints(); + + while (it != cell->PointIdsEnd()) + { + pointIdArray[n] = *it; + this->GetOutput()->GetPoint(*it, &pointArray[n]); + ++it; + ++n; + } + + for (unsigned int ii = 0; ii < 3; ++ii) + { + unsigned int jj = (ii + 1) % 3; + + InputQEType * edge = this->GetInput()->FindEdge(pointIdArray[ii], pointIdArray[jj]); + + if (!this->m_EdgesPointIdentifier->IndexExists(edge)) + { + InputPointType newPoint; + OutputPointType outPoint; + newPoint.SetToMidPoint(pointArray[ii], pointArray[jj]); + outPoint.CastFrom(newPoint); + + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + output->SetPoint(numberOfPoints, outPoint); + + ++numberOfPoints; + } + } +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..15e77640496e --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,97 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h +#define itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter + * + * \brief FIXME Add documentation here + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter + : public TriangleEdgeCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + + using Self = LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainer = typename Superclass::InputPointsContainer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerConstIterator = typename Superclass::InputPointsContainerConstIterator; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputCellsContainer = typename Superclass::InputCellsContainer; + using InputCellsContainerPointer = typename Superclass::InputCellsContainerPointer; + using InputCellsContainerIterator = typename Superclass::InputCellsContainerIterator; + using InputCellsContainerConstIterator = typename Superclass::InputCellsContainerConstIterator; + using InputPointType = typename Superclass::InputPointType; + using InputCoordType = typename Superclass::InputCoordinateType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputVectorType = typename Superclass::OutputVectorType; + using OutputCoordType = typename Superclass::OutputCoordType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + + /** New macro for creation of through a Smart Pointer */ + itkNewMacro(Self); + +protected: + LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter() = default; + ~LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + AddNewEdgePoints(InputQEType * edge) override; +}; +} // namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..70026ba43336 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,47 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ +#ifndef itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +void +LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter::AddNewEdgePoints(InputQEType * edge) +{ + OutputMeshType * output = this->GetOutput(); + + InputPointType pointArray[3]; + this->GetInput()->GetPoint(edge->GetOrigin(), &pointArray[0]); + this->GetInput()->GetPoint(edge->GetDestination(), &pointArray[1]); + pointArray[2].SetToMidPoint(pointArray[0], pointArray[1]); + + OutputPointType outpoint; + outpoint.CastFrom(pointArray[2]); + + OutputPointIdentifier numberOfPoints = output->GetNumberOfPoints(); + + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + output->SetPoint(numberOfPoints, outpoint); + + return; +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..06fd72a5816c --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,128 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter_h +#define itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class LoopTriangleCellSubdivisionQuadEdgeMeshFilter + * \brief Subdivide a triangular surface QuadEdgeMesh using Loop Subdivision + * + * The Loop subdivision scheme is a simple approximating face-split scheme for + * triangular meshes. The new points defined as: + * \f[ + * NV_k = \frac{3}{8} \sum_{i=1}^{2} U_k^i + \frac{1}{8} \sum_{i=1}^{2} V_k^i + * \f] + * + * where \f$NV_k\f$ is the new inserted points, \f$U_k\f$ are the two vertices + * of edge \f$k\f$, \f$V_k\f$ are two neighborhood vertices. In addition, the + * original vertices are smoothed, for each vertex in the original mesh + * \f[ + * OV_k = \left( 1- N \cdot B \right) \cdot OV_k + \beta \cdot \sum_{i=1}^{N} V_i + * \f] + * + * where \f$N\f$ denotes the number of vertices of first ring neighborhood + * points. \f$OVk\f$ is the original vertex. \f$V_i\f$ are first ring + * neighborhood points. The weighting \f$\beta\f$ defined as + * + * \f[ + * \beta = \frac{1}{N} \left( \frac{5}{8} - \left( \frac{3}{8} + \frac{1}{4} \cdot \cos^2\left(\frac{2\pi}{N} + * \right)\right) \right) \f] + * + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class LoopTriangleCellSubdivisionQuadEdgeMeshFilter + : public TriangleCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(LoopTriangleCellSubdivisionQuadEdgeMeshFilter); + + using Self = LoopTriangleCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainer = typename Superclass::InputPointsContainer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputPointsContainerConstIterator = typename Superclass::InputPointsContainerConstIterator; + using InputCellsContainer = typename Superclass::InputCellsContainer; + using InputCellsContainerPointer = typename Superclass::InputCellsContainerPointer; + using InputCellsContainerIterator = typename Superclass::InputCellsContainerIterator; + using InputCellsContainerConstIterator = typename Superclass::InputCellsContainerConstIterator; + using InputPointType = typename Superclass::InputPointType; + using InputVectorType = typename Superclass::InputVectorType; + using InputCoordType = typename Superclass::InputCoordType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputVectorType = typename Superclass::OutputVectorType; + using OutputCoordType = typename Superclass::OutputCoordType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + using SubdivisionCellContainerConstIterator = typename Superclass::SubdivisionCellContainerConstIterator; + + using EdgePointIdentifierContainer = typename Superclass::EdgePointIdentifierContainer; + using EdgePointIdentifierContainerPointer = typename Superclass::EdgePointIdentifierContainerPointer; + using EdgePointIdentifierContainerConstIterator = typename Superclass::EdgePointIdentifierContainerConstIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(LoopTriangleCellSubdivisionQuadEdgeMeshFilter); + + /** New macro for creation of through a Smart Pointer */ + itkNewMacro(Self); + +protected: + LoopTriangleCellSubdivisionQuadEdgeMeshFilter() = default; + ~LoopTriangleCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + CopyInputMeshToOutputMeshPoints() override; + + void + AddNewCellPoints(InputCellType * cell) override; + + InputPointType + SmoothingPoint(const InputPointType & ipt, const InputPointsContainer * points); +}; +} // namespace itk +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..86929738a164 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,238 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ +#ifndef itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter_hxx + +#include "itkMath.h" +#include + +namespace itk +{ +template +void +LoopTriangleCellSubdivisionQuadEdgeMeshFilter::AddNewCellPoints(InputCellType * cell) +{ + if (cell->GetType() != CellGeometryEnum::POLYGON_CELL || cell->GetNumberOfPoints() != 3) + { + itkExceptionMacro(<< " The input cell is not a triangle cell"); + } + + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + InputPointIdentifier n = 0; + OutputPointIdentifier pointIdArray[3]; + + InputCoordType pointWeight[4] = { 0.375, 0.375, 0.125, 0.125 }; + InputPointType pointArray[4]; + + OutputPointIdentifier numberOfPoints = output->GetNumberOfPoints(); + + InputPointIdIterator it = cell->PointIdsBegin(); + while (it != cell->PointIdsEnd()) + { + pointIdArray[n++] = *it; + ++it; + } + + for (unsigned int ii = 0; ii < 3; ++ii) + { + unsigned int jj = (ii + 1) % 3; + + InputQEType * edge = input->FindEdge(pointIdArray[ii], pointIdArray[jj]); + + if (!this->m_EdgesPointIdentifier->IndexExists(edge)) + { + if (edge->IsInternal()) + { + InputPointType newPoint; + newPoint.Fill(NumericTraits::Zero); + + input->GetPoint(pointIdArray[ii], &pointArray[0]); + input->GetPoint(pointIdArray[jj], &pointArray[1]); + + if (edge->GetLnext()) + { + input->GetPoint(edge->GetLnext()->GetDestination(), &pointArray[2]); + } + else + { + pointArray[2].Fill(NumericTraits::Zero); + } + + if (edge->GetRprev()) + { + input->GetPoint(edge->GetRprev()->GetDestination(), &pointArray[3]); + } + else + { + pointArray[3].Fill(NumericTraits::Zero); + } + + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; kk++) + { + for (unsigned int mm = 0; mm < 4; mm++) + { + newPoint[kk] += pointWeight[mm] * pointArray[mm][kk]; + } + } + + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + + OutputPointType outPoint; + outPoint.CastFrom(newPoint); + output->SetPoint(numberOfPoints, outPoint); + } + else if (edge->IsAtBorder()) + { + input->GetPoint(pointIdArray[ii], &pointArray[0]); + input->GetPoint(pointIdArray[jj], &pointArray[1]); + + InputPointType newPoint; + newPoint.SetToMidPoint(pointArray[0], pointArray[1]); + + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + + OutputPointType outPoint; + outPoint.CastFrom(newPoint); + output->SetPoint(numberOfPoints, outPoint); + } + else + { + itkExceptionMacro(<< "Wire edge detected"); + } + ++numberOfPoints; + } + } +} + +template +void +LoopTriangleCellSubdivisionQuadEdgeMeshFilter::CopyInputMeshToOutputMeshPoints() +{ + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + const InputPointsContainer * points = input->GetPoints(); + output->GetPoints()->Reserve(input->GetNumberOfPoints()); + + const InputCellsContainer * cells = input->GetCells(); + + if (this->m_Uniform) + { + InputPointsContainerConstIterator ptIt = points->Begin(); + while (ptIt != points->End()) + { + OutputPointType outpoint; + // outpoint.CastFrom( ptIt->Value() ); + outpoint.CastFrom(this->SmoothingPoint(ptIt->Value(), points)); + output->SetPoint(ptIt->Index(), outpoint); + ++ptIt; + } + } + else + { + std::set smoothedPointSet; + SubdivisionCellContainerConstIterator it = this->m_CellsToBeSubdivided.begin(); + SubdivisionCellContainerConstIterator end = this->m_CellsToBeSubdivided.end(); + while (it != end) + { + InputCellType * cell = cells->GetElement(static_cast(*it)); + InputPointIdIterator pter = cell->PointIdsBegin(); + while (pter != cell->PointIdsEnd()) + { + smoothedPointSet.insert(*pter); + ++pter; + } + + ++it; + } + + InputPointsContainerConstIterator ptIt = points->Begin(); + while (ptIt != points->End()) + { + InputPointType ipt = ptIt->Value(); + + if (smoothedPointSet.count(ptIt->Index())) + { + OutputPointType outpoint; + outpoint.CastFrom(this->SmoothingPoint(ptIt->Value(), points)); + output->SetPoint(ptIt->Index(), outpoint); + } + else + { + OutputPointType outpoint; + outpoint.CastFrom(ipt); + output->SetPoint(ptIt->Index(), outpoint); + } + ++ptIt; + } + } +} + +template +typename LoopTriangleCellSubdivisionQuadEdgeMeshFilter::InputPointType +LoopTriangleCellSubdivisionQuadEdgeMeshFilter::SmoothingPoint( + const InputPointType & ipt, + const InputPointsContainer * points) +{ + InputPointType opt; + opt.Fill(NumericTraits::Zero); + unsigned int nn = 0; + + InputPointType bpt; + bpt.Fill(NumericTraits::Zero); + unsigned int nb = 0; + + InputQEType * edge = ipt.GetEdge(); + typename InputQEType::IteratorGeom q_it = edge->BeginGeomOnext(); + while (q_it != edge->EndGeomOnext()) + { + if (q_it.Value()->IsAtBorder()) + { + bpt += points->ElementAt(q_it.Value()->GetDestination()).GetVectorFromOrigin(); + ++nb; + } + + opt += points->ElementAt(q_it.Value()->GetDestination()).GetVectorFromOrigin(); + ++nn; + ++q_it; + } + + if (nb) + { + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; ++kk) + { + opt[kk] = 0.75 * ipt[kk] + 0.125 * bpt[kk]; + } + } + else + { + InputCoordType var = 0.375 + 0.25 * std::cos(2.0 * itk::Math::pi / nn); + InputCoordType beta = (0.625 - var * var) / nn; + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; ++kk) + { + opt[kk] = (1.0 - nn * beta) * ipt[kk] + beta * opt[kk]; + } + } + return opt; +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..1d6df0d95daa --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,103 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h +#define itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter + * + * \brief FIXME Add documentation here + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter + : public TriangleEdgeCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + + using Self = LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainer = typename Superclass::InputPointsContainer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerConstIterator = typename Superclass::InputPointsContainerConstIterator; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputCellsContainer = typename Superclass::InputCellsContainer; + using InputCellsContainerPointer = typename Superclass::InputCellsContainerPointer; + using InputCellsContainerIterator = typename Superclass::InputCellsContainerIterator; + using InputCellsContainerConstIterator = typename Superclass::InputCellsContainerConstIterator; + using InputPointType = typename Superclass::InputPointType; + using InputCoordType = typename Superclass::InputCoordinateType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputVectorType = typename Superclass::OutputVectorType; + using OutputCoordType = typename Superclass::OutputCoordType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + + /** New macro for creation of through a Smart Pointer */ + itkNewMacro(Self); + +protected: + LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter() = default; + ~LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + AddNewEdgePoints(InputQEType * edge) override; + + void + CopyInputMeshToOutputMeshPoints() override; + + virtual void + AverageOriginOfEdge(InputQEType * edge, const InputPointsContainer * points); +}; +} // namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..d242290fe22c --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,202 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ +#ifndef itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx + +#include "itkMath.h" +#include + +namespace itk +{ +template +void +LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter::AddNewEdgePoints(InputQEType * edge) +{ + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + if (edge->IsInternal()) + { + InputCoordType pointWeight[4] = { 0.375, 0.375, 0.125, 0.125 }; + InputPointType pointArray[4]; + + InputPointType newPoint; + newPoint.Fill(NumericTraits::Zero); + + input->GetPoint(edge->GetOrigin(), &pointArray[0]); + input->GetPoint(edge->GetDestination(), &pointArray[1]); + + if (edge->GetLnext()) + { + input->GetPoint(edge->GetLnext()->GetDestination(), &pointArray[2]); + } + else + { + pointArray[2].Fill(NumericTraits::Zero); + } + + if (edge->GetRprev()) + { + input->GetPoint(edge->GetRprev()->GetDestination(), &pointArray[3]); + } + else + { + pointArray[3].Fill(NumericTraits::Zero); + } + + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; kk++) + { + for (unsigned int mm = 0; mm < 4; mm++) + { + newPoint[kk] += pointWeight[mm] * pointArray[mm][kk]; + } + } + + OutputPointIdentifier numberOfPoints = output->GetNumberOfPoints(); + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + + OutputPointType outPoint; + outPoint.CastFrom(newPoint); + output->SetPoint(numberOfPoints, outPoint); + } + else if (edge->IsAtBorder()) + { + InputPointType pointArray[2]; + + input->GetPoint(edge->GetOrigin(), &pointArray[0]); + input->GetPoint(edge->GetDestination(), &pointArray[1]); + + InputPointType newPoint; + newPoint.SetToMidPoint(pointArray[0], pointArray[1]); + + OutputPointIdentifier numberOfPoints = output->GetNumberOfPoints(); + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + + OutputPointType outPoint; + outPoint.CastFrom(newPoint); + output->SetPoint(numberOfPoints, outPoint); + } + else + { + itkExceptionMacro(<< "Wire edge detected"); + } + return; +} + +template +void +LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter::CopyInputMeshToOutputMeshPoints() +{ + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + const InputPointsContainer * points = input->GetPoints(); + output->GetPoints()->Reserve(input->GetNumberOfPoints()); + + InputPointsContainerConstIterator ptIt = points->Begin(); + while (ptIt != points->End()) + { + InputQEType * edge = ptIt->Value().GetEdge(); + InputQEType * qter0 = edge; + InputQEType * qter1 = edge->GetOnext(); + + bool copyFlag = true; + while (qter1 != edge) + { + if (std::find(this->m_EdgesToBeSubdivided.begin(), this->m_EdgesToBeSubdivided.end(), qter0) != + this->m_EdgesToBeSubdivided.end()) + { + break; + copyFlag = false; + } + + qter0 = qter1; + qter1 = qter0->GetOnext(); + } + + if (copyFlag) + { + OutputPointType outpoint; + outpoint.CastFrom(ptIt->Value()); + this->GetOutput()->SetPoint(ptIt->Index(), outpoint); + } + else + { + this->AverageOriginOfEdge(edge, points); + } + ++ptIt; + } +} + +template +void +LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter::AverageOriginOfEdge( + InputQEType * edge, + const InputPointsContainer * points) +{ + InputPointType opt; + opt.Fill(NumericTraits::Zero); + + InputPointType bpt; + bpt.Fill(NumericTraits::Zero); + + InputPointType ipt = points->ElementAt(edge->GetOrigin()); + + unsigned int nb = 0; + unsigned int nn = 0; + + typename InputQEType::IteratorGeom q_it = edge->BeginGeomOnext(); + while (q_it != edge->EndGeomOnext()) + { + if (q_it.Value()->IsAtBorder()) + { + bpt += points->ElementAt(q_it.Value()->GetDestination()).GetVectorFromOrigin(); + ++nb; + } + + opt += points->ElementAt(q_it.Value()->GetDestination()).GetVectorFromOrigin(); + ++nn; + ++q_it; + } + + if (nb) + { + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; ++kk) + { + opt[kk] = 0.75 * ipt[kk] + 0.125 * bpt[kk]; + } + } + else + { + InputCoordType var = 0.375 + 0.25 * std::cos(2.0 * itk::Math::pi / nn); + InputCoordType beta = (0.625 - var * var) / nn; + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; ++kk) + { + opt[kk] = (1.0 - nn * beta) * ipt[kk] + beta * opt[kk]; + } + } + + OutputPointType outpoint; + outpoint.CastFrom(opt); + this->GetOutput()->SetPoint(edge->GetOrigin(), outpoint); +} + +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..56025453d41d --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,99 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter_h +#define itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter + * \brief Interpolating subdivision scheme. + * + * Similar to LinearTriangleCellSubdivisionQuadEdgeMeshFilter, except that new vertices created using butterfly + * neighborhood: \f[ NV_k = \frac{1}{2} \sum_{i=1}{2} U_k^i + \frac{1}{8} \sum_{i=1}^{2} V_k^i - \frac{1}{16} + * \sum_{i=1}{4} W_k^i \f] + * + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter + : public TriangleCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter); + + using Self = ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputPointType = typename Superclass::InputPointType; + using InputVectorType = typename Superclass::InputVectorType; + using InputCoordType = typename Superclass::InputCoordType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputVectorType = typename Superclass::OutputVectorType; + using OutputCoordType = typename Superclass::OutputCoordType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + using EdgePointIdentifierContainer = typename Superclass::EdgePointIdentifierContainer; + using EdgePointIdentifierContainerPointer = typename Superclass::EdgePointIdentifierContainerPointer; + using EdgePointIdentifierContainerConstIterator = typename Superclass::EdgePointIdentifierContainerConstIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter); + + /** New macro for creation of through a Smart Pointer */ + itkNewMacro(Self); + +protected: + ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter() = default; + ~ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + AddNewCellPoints(InputCellType * cell) override; +}; +} // namespace itk +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..b383a483dd5e --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,147 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ +#ifndef itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter_hxx + +#include "itkNumericTraits.h" + +namespace itk +{ +template +void +ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter::AddNewCellPoints( + InputCellType * cell) +{ + if (cell->GetType() != CellGeometryEnum::POLYGON_CELL || cell->GetNumberOfPoints() != 3) + { + itkExceptionMacro(<< " The input cell is not a triangle cell"); + } + + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + InputPointIdentifier n = 0; + InputPointIdentifier pointIdArray[3]; + + InputCoordType pointWeight[8] = { 0.5, 0.5, 0.125, 0.125, -0.0625, -0.0625, -0.0625, -0.0625 }; + InputPointType pointArray[8]; + + OutputPointIdentifier numberOfPoints = output->GetNumberOfPoints(); + + InputPointIdIterator it = cell->PointIdsBegin(); + while (it != cell->PointIdsEnd()) + { + pointIdArray[n++] = *it; + ++it; + } + + for (unsigned int ii = 0; ii < 3; ++ii) + { + unsigned int jj = (ii + 1) % 3; + + InputQEType * edge = input->FindEdge(pointIdArray[ii], pointIdArray[jj]); + + if (!this->m_EdgesPointIdentifier->IndexExists(edge)) + { + InputPointType newPoint; + newPoint.Fill(NumericTraits::Zero); + + input->GetPoint(pointIdArray[ii], &pointArray[0]); + input->GetPoint(pointIdArray[jj], &pointArray[1]); + + if (edge->GetLnext()) + { + InputPointIdentifier ptId = edge->GetLnext()->GetDestination(); + input->GetPoint(ptId, &pointArray[2]); + + if (edge->GetLnext()->GetRprev()) + { + ptId = edge->GetLnext()->GetRprev()->GetDestination(); + input->GetPoint(ptId, &pointArray[4]); + } + else + { + pointArray[4].Fill(NumericTraits::Zero); + } + } + else + { + pointArray[2].Fill(NumericTraits::Zero); + pointArray[4].Fill(NumericTraits::Zero); + } + + if (edge->GetRprev()) + { + InputPointIdentifier ptId = edge->GetRprev()->GetDestination(); + input->GetPoint(ptId, &pointArray[3]); + if (edge->GetRprev()->GetLnext()) + { + ptId = edge->GetRprev()->GetLnext()->GetDestination(); + input->GetPoint(ptId, &pointArray[5]); + } + else + { + pointArray[5].Fill(NumericTraits::Zero); + } + } + else + { + pointArray[3].Fill(NumericTraits::Zero); + pointArray[5].Fill(NumericTraits::Zero); + } + + if (edge->GetLprev() && edge->GetLprev()->GetRprev()) + { + InputPointIdentifier ptId = edge->GetLprev()->GetRprev()->GetDestination(); + input->GetPoint(ptId, &pointArray[6]); + } + else + { + pointArray[6].Fill(NumericTraits::Zero); + } + + if (edge->GetRnext() && edge->GetRnext()->GetLnext()) + { + InputPointIdentifier ptId = edge->GetRnext()->GetLnext()->GetDestination(); + input->GetPoint(ptId, &pointArray[7]); + } + else + { + pointArray[7].Fill(NumericTraits::Zero); + } + + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; ++kk) + { + for (unsigned int mm = 0; mm < 8; ++mm) + { + newPoint[kk] += pointWeight[mm] * pointArray[mm][kk]; + } + } + + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + + OutputPointType outPoint; + outPoint.CastFrom(newPoint); + output->SetPoint(numberOfPoints, outPoint); + ++numberOfPoints; + } + } +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..9b55406f66a3 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,98 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h +#define itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter + * + * \brief FIXME Add documentation here + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter + : public TriangleEdgeCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + + using Self = ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainer = typename Superclass::InputPointsContainer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerConstIterator = typename Superclass::InputPointsContainerConstIterator; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputCellsContainer = typename Superclass::InputCellsContainer; + using InputCellsContainerPointer = typename Superclass::InputCellsContainerPointer; + using InputCellsContainerIterator = typename Superclass::InputCellsContainerIterator; + using InputCellsContainerConstIterator = typename Superclass::InputCellsContainerConstIterator; + using InputPointType = typename Superclass::InputPointType; + using InputCoordType = typename Superclass::InputCoordinateType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputVectorType = typename Superclass::OutputVectorType; + using OutputCoordType = typename Superclass::OutputCoordType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + /** Run-time type information (and related methods). */ + itkTypeMacro(ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter, + TriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + + /** New macro for creation of through a Smart Pointer */ + itkNewMacro(Self); + +protected: + ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter() = default; + ~ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + AddNewEdgePoints(InputQEType * edge) override; +}; +} // namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..aec4563b0236 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,122 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ +#ifndef itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +void +ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter::AddNewEdgePoints( + InputQEType * edge) +{ + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + InputCoordType pointWeight[8] = { 0.5, 0.5, 0.125, 0.125, -0.0625, -0.0625, -0.0625, -0.0625 }; + InputPointType pointArray[8]; + + InputPointType newPoint; + newPoint.Fill(NumericTraits::Zero); + + input->GetPoint(edge->GetOrigin(), &pointArray[0]); + input->GetPoint(edge->GetDestination(), &pointArray[1]); + + if (edge->GetLnext()) + { + InputPointIdentifier ptId = edge->GetLnext()->GetDestination(); + input->GetPoint(ptId, &pointArray[2]); + + if (edge->GetLnext()->GetRprev()) + { + ptId = edge->GetLnext()->GetRprev()->GetDestination(); + input->GetPoint(ptId, &pointArray[4]); + } + else + { + pointArray[4].Fill(NumericTraits::Zero); + } + } + else + { + pointArray[2].Fill(NumericTraits::Zero); + pointArray[4].Fill(NumericTraits::Zero); + } + + if (edge->GetRprev()) + { + InputPointIdentifier ptId = edge->GetRprev()->GetDestination(); + input->GetPoint(ptId, &pointArray[3]); + if (edge->GetRprev()->GetLnext()) + { + ptId = edge->GetRprev()->GetLnext()->GetDestination(); + input->GetPoint(ptId, &pointArray[5]); + } + else + { + pointArray[5].Fill(NumericTraits::Zero); + } + } + else + { + pointArray[3].Fill(NumericTraits::Zero); + pointArray[5].Fill(NumericTraits::Zero); + } + + if (edge->GetLprev() && edge->GetLprev()->GetRprev()) + { + InputPointIdentifier ptId = edge->GetLprev()->GetRprev()->GetDestination(); + input->GetPoint(ptId, &pointArray[6]); + } + else + { + pointArray[6].Fill(NumericTraits::Zero); + } + + if (edge->GetRnext() && edge->GetRnext()->GetLnext()) + { + InputPointIdentifier ptId = edge->GetRnext()->GetLnext()->GetDestination(); + input->GetPoint(ptId, &pointArray[7]); + } + else + { + pointArray[7].Fill(NumericTraits::Zero); + } + + for (unsigned int kk = 0; kk < InputMeshType::PointDimension; ++kk) + { + for (unsigned int mm = 0; mm < 8; ++mm) + { + newPoint[kk] += pointWeight[mm] * pointArray[mm][kk]; + } + } + + OutputPointIdentifier numberOfPoints = output->GetNumberOfPoints(); + + this->m_EdgesPointIdentifier->InsertElement(edge, numberOfPoints); + this->m_EdgesPointIdentifier->InsertElement(edge->GetSym(), numberOfPoints); + + OutputPointType outPoint; + outPoint.CastFrom(newPoint); + output->SetPoint(numberOfPoints, outPoint); + + return; +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkQuadEdgeMeshSubdivisionCriterion.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkQuadEdgeMeshSubdivisionCriterion.h new file mode 100644 index 000000000000..b7e23516193a --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkQuadEdgeMeshSubdivisionCriterion.h @@ -0,0 +1,71 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkQuadEdgeMeshSubdivisionCriterion_h +#define itkQuadEdgeMeshSubdivisionCriterion_h + +#include "itkObject.h" + +namespace itk +{ +/** + *\class QuadEdgeMeshSubdivisionCriterion + *\brief + *\ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class ITK_EXPORT QuadEdgeMeshSubdivisionCriterion : public Object +{ +public: + using Self = QuadEdgeMeshSubdivisionCriterion; + using Superclass = Object; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using MeshType = typename TCellSubdivisionFilter::InputMeshType; + using MeshPointer = typename MeshType::Pointer; + using MeshConstPointer = typename MeshType::ConstPointer; + using PointsContainerPointer = typename MeshType::PointsContainerPointer; + using PointsContainerConstIterator = typename MeshType::PointsContainerConstIterator; + using PointsContainerIterator = typename MeshType::PointsContainerIterator; + using CellsContainer = typename MeshType::CellsContainer; + using CellsContainerPointer = typename MeshType::CellsContainerPointer; + using CellsContainerIterator = typename MeshType::CellsContainerIterator; + using CellsContainerConstIterator = typename MeshType::CellsContainerConstIterator; + using PointType = typename MeshType::PointType; + using CoordinateType = typename MeshType::CoordinateType; + using PointIdentifier = typename MeshType::PointIdentifier; + using CellIdentifier = typename MeshType::CellIdentifier; + using CellType = typename MeshType::CellType; + using QEType = typename MeshType::QEType; + using PointIdIterator = typename MeshType::PointIdIterator; + using SubdivisionCellContainer = typename TCellSubdivisionFilter::SubdivisionCellContainer; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(QuadEdgeMeshSubdivisionCriterion); + + virtual void + Compute(MeshType * mesh, SubdivisionCellContainer & edgeList) = 0; + +protected: + QuadEdgeMeshSubdivisionCriterion() = default; + ~QuadEdgeMeshSubdivisionCriterion() override = default; +}; + +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..19784e03f77a --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,105 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter_h +#define itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter + * + * \brief FIXME Add documentation here + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter + : public TriangleCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter); + + using Self = SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputCellsContainer = typename Superclass::InputCellsContainer; + using InputCellsContainerPointer = typename Superclass::InputCellsContainerPointer; + using InputCellsContainerIterator = typename Superclass::InputCellsContainerIterator; + using InputCellsContainerConstIterator = typename Superclass::InputCellsContainerConstIterator; + using InputPointType = typename Superclass::InputPointType; + using InputVectorType = typename Superclass::InputVectorType; + using InputCoordType = typename Superclass::InputCoordType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputCellsContainerPointer = typename Superclass::OutputCellsContainerPointer; + using OutputCellsContainerIterator = typename Superclass::OutputCellsContainerIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputVectorType = typename Superclass::OutputVectorType; + using OutputCoordType = typename Superclass::OutputCoordType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + using EdgePointIdentifierContainer = typename Superclass::EdgePointIdentifierContainer; + using EdgePointIdentifierContainerPointer = typename Superclass::EdgePointIdentifierContainerPointer; + using EdgePointIdentifierContainerIterator = typename Superclass::EdgePointIdentifierContainerIterator; + using EdgePointIdentifierContainerConstIterator = typename Superclass::EdgePointIdentifierContainerConstIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter); + + /** New macro for creation of through a Smart Pointer */ + itkNewMacro(Self); + +protected: + SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter() = default; + ~SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + AddNewCellPoints(InputCellType * cell) override; + void + GenerateOutputCells() override; +}; +} // namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..34599b3ca73c --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,190 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ +#ifndef itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +void +SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter::AddNewCellPoints(InputCellType * cell) +{ + if (cell->GetType() != CellGeometryEnum::POLYGON_CELL || cell->GetNumberOfPoints() != 3) + { + itkExceptionMacro(<< " The input cell is not a triangle cell"); + } + + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + InputPointIdentifier pointIdArray[3]; + + InputPointType newPoint; + newPoint.Fill(NumericTraits::Zero); + + InputPointIdIterator pter = cell->PointIdsBegin(); + InputPointIdentifier nn = 0; + + while (pter != cell->PointIdsEnd()) + { + pointIdArray[nn] = *pter; + InputPointType cellPoint = input->GetPoints()->ElementAt(*pter); + newPoint += cellPoint.GetVectorFromOrigin(); + ++pter; + ++nn; + } + + typename InputPointType::ValueType den = 1. / static_cast(nn); + + if (den < NumericTraits::epsilon()) + { + itkExceptionMacro("Exception caused by unusual large number of points inside a cell"); + } + + for (unsigned int kk = 0; kk < InputPointType::PointDimension; ++kk) + { + newPoint[kk] *= den; + } + + OutputPointType outPoint; + outPoint.CastFrom(newPoint); + OutputPointIdentifier newPointId = output->GetNumberOfPoints(); + output->SetPoint(newPointId, outPoint); + + for (unsigned int ii = 0; ii < 3; ++ii) + { + unsigned int jj = (ii + 1) % 3; + InputQEType * edge = input->FindEdge(pointIdArray[ii], pointIdArray[jj]); + + if (edge && !this->m_EdgesPointIdentifier->IndexExists(edge)) + { + this->m_EdgesPointIdentifier->InsertElement(edge, newPointId); + } + } +} + +template +void +SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter::GenerateOutputCells() +{ + // 1. Get input and output + const InputMeshType * input = this->GetInput(); + OutputMeshType * output = this->GetOutput(); + + // 2. Get cells container from input + const InputCellsContainer * cells = input->GetCells(); + + // 3. Clear cells to be subdivided. + this->m_CellsToBeSubdivided.clear(); + + // 4. Copy all the cell that does not insert a new point. + InputCellsContainerConstIterator cellIt = cells->Begin(); + while (cellIt != cells->End()) + { + InputCellType * cell = cellIt->Value(); + + if (cell->GetType() != CellGeometryEnum::POLYGON_CELL || cell->GetNumberOfPoints() != 3) + { + ++cellIt; + continue; + } + + InputPointIdentifier cellPointIdArray[3]; + + InputPointIdIterator it = cell->PointIdsBegin(); + unsigned int n = 0; + + while (it != cell->PointIdsEnd()) + { + cellPointIdArray[n] = *it; + ++it; + ++n; + } + + for (unsigned int ii = 0; ii < 3; ++ii) + { + unsigned int jj = (ii + 1) % 3; + + InputQEType * edge = input->FindEdge(cellPointIdArray[ii], cellPointIdArray[jj]); + + if (!this->m_EdgesPointIdentifier->IndexExists(edge)) + { + // No added new point in this triangle cell + const auto qeprimal = output->AddFaceTriangle(static_cast(cellPointIdArray[0]), + static_cast(cellPointIdArray[1]), + static_cast(cellPointIdArray[2])); + this->PassCellData(cellIt.Index(), qeprimal); + break; + } + else if (this->m_EdgesPointIdentifier->ElementAt(edge) != NumericTraits::max()) + { + // With a new added point in this triangle cell. + OutputPointIdentifier pointIdArray[2][2]; + pointIdArray[0][0] = static_cast(edge->GetOrigin()); + pointIdArray[0][1] = static_cast(edge->GetDestination()); + pointIdArray[1][0] = this->m_EdgesPointIdentifier->ElementAt(edge); + + if (edge->IsAtBorder() || !this->m_EdgesPointIdentifier->IndexExists(edge->GetSym())) + { + // There is no neighbor triangle cell or no splitting of the neighbor triangle cell. + if (this->m_Uniform) + { + const auto qeprimal = output->AddFaceTriangle(pointIdArray[0][0], pointIdArray[0][1], pointIdArray[1][0]); + this->PassCellData(cellIt.Index(), qeprimal); + } + else + { + OutputQEType * newTriangleEdge = + output->AddFaceTriangle(pointIdArray[0][0], pointIdArray[0][1], pointIdArray[1][0]); + this->PassCellData(cellIt.Index(), newTriangleEdge); + this->m_CellsToBeSubdivided.push_back(newTriangleEdge->GetLeft()); + } + } + else + { + // There is neighbor triangle cell was split as well, swapping edges was conducted. + pointIdArray[1][1] = this->m_EdgesPointIdentifier->ElementAt(edge->GetSym()); + if (this->m_Uniform) + { + const auto qeprimal0 = output->AddFaceTriangle(pointIdArray[1][0], pointIdArray[1][1], pointIdArray[0][1]); + this->PassCellData(cellIt.Index(), qeprimal0); + const auto qeprimal1 = output->AddFaceTriangle(pointIdArray[1][1], pointIdArray[1][0], pointIdArray[0][0]); + this->PassCellData(cellIt.Index(), qeprimal1); + } + else + { + OutputQEType * newTriangleEdge = + output->AddFaceTriangle(pointIdArray[1][0], pointIdArray[1][1], pointIdArray[0][1]); + this->PassCellData(cellIt.Index(), newTriangleEdge); + this->m_CellsToBeSubdivided.push_back(newTriangleEdge->GetLeft()); + + newTriangleEdge = output->AddFaceTriangle(pointIdArray[1][1], pointIdArray[1][0], pointIdArray[0][0]); + this->PassCellData(cellIt.Index(), newTriangleEdge); + this->m_CellsToBeSubdivided.push_back(newTriangleEdge->GetLeft()); + } + this->m_EdgesPointIdentifier->SetElement(edge->GetSym(), NumericTraits::max()); + } + } + } + ++cellIt; + } +} + +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..0be0e567de65 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,120 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkSubdivisionQuadEdgeMeshFilter_h +#define itkSubdivisionQuadEdgeMeshFilter_h + +#include "itkConceptChecking.h" +#include "itkQuadEdgeMeshToQuadEdgeMeshFilter.h" +#include "itkMapContainer.h" + +namespace itk +{ +/** + * \class SubdivisionQuadEdgeMeshFilter + * \brief Abstract base class for itk::QuadEdgeMesh subdivision + * + * Code imported from Insight Journal publication: + * + * Wanlin Zhu, Triangle Mesh Subdivision + * https://hdl.handle.net/10380/3307 + * + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class SubdivisionQuadEdgeMeshFilter : public QuadEdgeMeshToQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(SubdivisionQuadEdgeMeshFilter); + + using Self = SubdivisionQuadEdgeMeshFilter; + using Superclass = QuadEdgeMeshToQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = TInputMesh; + using InputMeshPointer = typename InputMeshType::Pointer; + using InputMeshConstPointer = typename InputMeshType::ConstPointer; + using InputPointsContainer = typename InputMeshType::PointsContainer; + using InputPointsContainerPointer = typename InputMeshType::PointsContainerPointer; + using InputPointsContainerConstIterator = typename InputMeshType::PointsContainerConstIterator; + using InputPointsContainerIterator = typename InputMeshType::PointsContainerIterator; + using InputCellsContainer = typename InputMeshType::CellsContainer; + using InputCellsContainerPointer = typename InputMeshType::CellsContainerPointer; + using InputCellsContainerIterator = typename InputMeshType::CellsContainerIterator; + using InputCellsContainerConstIterator = typename InputMeshType::CellsContainerConstIterator; + using InputPointType = typename InputMeshType::PointType; + using InputCoordType = typename InputMeshType::CoordinateType; + using InputPointIdentifier = typename InputMeshType::PointIdentifier; + using InputCellIdentifier = typename InputMeshType::CellIdentifier; + using InputCellType = typename InputMeshType::CellType; + using InputQEType = typename InputMeshType::QEType; + using InputMeshTraits = typename InputMeshType::MeshTraits; + using InputPointIdIterator = typename InputMeshType::PointIdIterator; + + using OutputMeshType = TOutputMesh; + using OutputMeshPointer = typename OutputMeshType::Pointer; + using OutputPointsContainerPointer = typename OutputMeshType::PointsContainerPointer; + using OutputPointsContainerIterator = typename OutputMeshType::PointsContainerIterator; + using OutputCellsContainer = typename OutputMeshType::CellsContainer; + using OutputCellsContainerPointer = typename OutputMeshType::CellsContainerPointer; + using OutputCellsContainerIterator = typename OutputMeshType::CellsContainerIterator; + using OutputCellsContainerConstIterator = typename OutputMeshType::CellsContainerConstIterator; + using OutputPointType = typename OutputMeshType::PointType; + using OutputCoordType = typename OutputMeshType::CoordinateType; + using OutputPointIdentifier = typename OutputMeshType::PointIdentifier; + using OutputCellIdentifier = typename OutputMeshType::CellIdentifier; + using OutputCellType = typename OutputMeshType::CellType; + using OutputQEType = typename OutputMeshType::QEType; + using OutputMeshTraits = typename OutputMeshType::MeshTraits; + using OutputPointIdIterator = typename OutputMeshType::PointIdIterator; + + using EdgePointIdentifierContainer = MapContainer; + using EdgePointIdentifierContainerPointer = typename EdgePointIdentifierContainer::Pointer; + using EdgePointIdentifierContainerIterator = typename EdgePointIdentifierContainer::Iterator; + using EdgePointIdentifierContainerConstIterator = typename EdgePointIdentifierContainer::ConstIterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(SubdivisionQuadEdgeMeshFilter); + +protected: + SubdivisionQuadEdgeMeshFilter(); + ~SubdivisionQuadEdgeMeshFilter() override = default; + + /** inheriting class should implement this method, to take care of mesh geometry (vertex' coordinates). */ + virtual void + GenerateOutputPoints() = 0; + + /** inheriting class should implement this method, to take care of mesh connectivity (vertex' connection). */ + virtual void + GenerateOutputCells() = 0; + void + GenerateData() override; + + void + PrintSelf(std::ostream & os, Indent indent) const override; + + EdgePointIdentifierContainerPointer m_EdgesPointIdentifier; +}; +} // end namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..44fc74286c7f --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,48 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkSubdivisionQuadEdgeMeshFilter_hxx +#define itkSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +SubdivisionQuadEdgeMeshFilter::SubdivisionQuadEdgeMeshFilter() +{ + this->m_EdgesPointIdentifier = EdgePointIdentifierContainer::New(); +} + +template +void +SubdivisionQuadEdgeMeshFilter::GenerateData() +{ + + this->GenerateOutputPoints(); + + this->GenerateOutputCells(); +} + +template +void +SubdivisionQuadEdgeMeshFilter::PrintSelf(std::ostream & os, Indent indent) const +{ + Superclass::PrintSelf(os, indent); +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..72e81972f47c --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,142 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkTriangleCellSubdivisionQuadEdgeMeshFilter_h +#define itkTriangleCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class TriangleCellSubdivisionQuadEdgeMeshFilter + * \brief Abstract class to subdivide triangular surface QuadEdgeMesh. + * + * If m_Uniform is true, then all faces are subdivided. Else only faces added + * by the means of SetCellsToBeSubdivided or AddSubdividedCellId are going to + * be subdivided using the corresponding subdivision scheme. Then neighbor + * faces could be subdivided depending on their surrounding (to maintain + * surface genus). + * + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class TriangleCellSubdivisionQuadEdgeMeshFilter : public SubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(TriangleCellSubdivisionQuadEdgeMeshFilter); + + using Self = TriangleCellSubdivisionQuadEdgeMeshFilter; + using Superclass = SubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainer = typename Superclass::InputPointsContainer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerConstIterator = typename Superclass::InputPointsContainerConstIterator; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputCellsContainer = typename Superclass::InputCellsContainer; + using InputCellsContainerPointer = typename Superclass::InputCellsContainerPointer; + using InputCellsContainerIterator = typename Superclass::InputCellsContainerIterator; + using InputCellsContainerConstIterator = typename Superclass::InputCellsContainerConstIterator; + using InputPointType = typename Superclass::InputPointType; + using InputCoordType = typename Superclass::InputCoordinateType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputCellsContainer = typename Superclass::OutputCellsContainer; + using OutputCellsContainerPointer = typename Superclass::OutputCellsContainerPointer; + using OutputCellsContainerIterator = typename Superclass::OutputCellsContainerIterator; + using OutputCellsContainerConstIterator = typename Superclass::OutputCellsContainerConstIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputCoordType = typename Superclass::OutputCoordinateType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + using SubdivisionCellContainer = std::list; + using SubdivisionCellContainerConstIterator = typename SubdivisionCellContainer::const_iterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(TriangleCellSubdivisionQuadEdgeMeshFilter); + itkGetConstReferenceMacro(CellsToBeSubdivided, SubdivisionCellContainer); + + void + SetCellsToBeSubdivided(const SubdivisionCellContainer & cellIdList); + void + AddSubdividedCellId(OutputCellIdentifier cellId); + +protected: + TriangleCellSubdivisionQuadEdgeMeshFilter(); + ~TriangleCellSubdivisionQuadEdgeMeshFilter() override = default; + + virtual void + AddNewCellPoints(InputCellType * cell) = 0; + void + GenerateOutputPoints() override; + void + GenerateOutputCells() override; + + void + SplitTriangleFromOneEdge(OutputMeshType * output, + const OutputPointIdentifier * trianglePointIds, + const OutputPointIdentifier * edgePointIds, + const unsigned int * splitEdges, + const InputCellIdentifier id); + void + SplitTriangleFromTwoEdges(OutputMeshType * output, + const OutputPointIdentifier * trianglePointIds, + const OutputPointIdentifier * edgePointIds, + const unsigned int * splitEdges, + const InputCellIdentifier id); + void + SplitTriangleFromThreeEdges(OutputMeshType * output, + const OutputPointIdentifier * trianglePointIds, + const OutputPointIdentifier * edgePointIds, + const InputCellIdentifier id); + + void + PassCellData(const InputCellIdentifier inputId, const OutputQEType * outputEdge); + + void + PrintSelf(std::ostream & os, Indent indent) const override; + + SubdivisionCellContainer m_CellsToBeSubdivided; + bool m_Uniform; +}; +} // end namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkTriangleCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..81226519b29d --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,296 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkTriangleCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkTriangleCellSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +TriangleCellSubdivisionQuadEdgeMeshFilter::TriangleCellSubdivisionQuadEdgeMeshFilter() +{ + this->m_Uniform = true; +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::SetCellsToBeSubdivided( + const SubdivisionCellContainer & cellIdList) +{ + this->m_CellsToBeSubdivided = cellIdList; + this->Modified(); +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::AddSubdividedCellId(OutputCellIdentifier cellId) +{ + this->m_CellsToBeSubdivided.push_back(cellId); + this->Modified(); +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::GenerateOutputPoints() +{ + this->CopyInputMeshToOutputMeshPoints(); + + this->m_EdgesPointIdentifier->Initialize(); + + this->m_Uniform = this->m_CellsToBeSubdivided.empty(); + + const InputCellsContainer * cells = this->GetInput()->GetCells(); + + if (this->m_Uniform) + { + InputCellsContainerConstIterator cellIt = cells->Begin(); + while (cellIt != cells->End()) + { + this->AddNewCellPoints(cellIt->Value()); + ++cellIt; + } + } + else + { + SubdivisionCellContainerConstIterator it = this->m_CellsToBeSubdivided.begin(); + SubdivisionCellContainerConstIterator end = this->m_CellsToBeSubdivided.end(); + while (it != end) + { + InputCellType * cell = cells->GetElement(static_cast(*it)); + if (cell) + { + this->AddNewCellPoints(cell); + } + ++it; + } + } +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::GenerateOutputCells() +{ + InputMeshConstPointer input = this->GetInput(); + OutputMeshPointer output = this->GetOutput(); + + this->m_CellsToBeSubdivided.clear(); + + const InputCellsContainer * cells = input->GetCells(); + + InputCellsContainerConstIterator cellIt = cells->Begin(); + + while (cellIt != cells->End()) + { + InputCellType * cell = cellIt->Value(); + + if (!cell || cell->GetType() != CellGeometryEnum::POLYGON_CELL || cell->GetNumberOfPoints() != 3) + { + ++cellIt; + continue; + } + + InputPointIdentifier inputPointIdArray[3]; + OutputPointIdentifier trianglePointIds[3]; + OutputPointIdentifier edgePointIds[3]; + + InputPointIdIterator it = cell->PointIdsBegin(); + unsigned int n = 0; + + while (it != cell->PointIdsEnd()) + { + inputPointIdArray[n] = *it; + trianglePointIds[n] = static_cast(inputPointIdArray[n]); + ++it; + ++n; + } + + unsigned int splitEdges[3]; + n = 0; + InputQEType * edge; + for (unsigned int ii = 0; ii < 3; ++ii) + { + unsigned int jj = (ii + 1) % 3; + + edge = input->FindEdge(inputPointIdArray[ii], inputPointIdArray[jj]); + + if (this->m_EdgesPointIdentifier->IndexExists(edge)) + { + edgePointIds[ii] = this->m_EdgesPointIdentifier->GetElement(edge); + splitEdges[n] = ii; + ++n; + } + } + + if (n == 0) + { + // this face has no subdivided face as neighbor, copy it + const auto qeprimal = output->AddFaceTriangle(trianglePointIds[0], trianglePointIds[1], trianglePointIds[2]); + this->PassCellData(cellIt.Index(), qeprimal); + } + else if (n == 1) + { + SplitTriangleFromOneEdge(output, trianglePointIds, edgePointIds, splitEdges, cellIt.Index()); + } + else if (n == 2) + { + SplitTriangleFromTwoEdges(output, trianglePointIds, edgePointIds, splitEdges, cellIt.Index()); + } + else if (n == 3) + { + // this face was not supposed to be subdivided but all neighbors are + SplitTriangleFromThreeEdges(output, trianglePointIds, edgePointIds, cellIt.Index()); + } + + ++cellIt; + } +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::SplitTriangleFromOneEdge( + OutputMeshType * output, + const OutputPointIdentifier * trianglePointIds, + const OutputPointIdentifier * edgePointIds, + const unsigned int * splitEdges, + const InputCellIdentifier id) +{ + unsigned int ii = splitEdges[0]; + unsigned int jj = (ii + 1) % 3; + unsigned int kk = (ii + 2) % 3; + + const auto qeprimal0 = output->AddFaceTriangle(edgePointIds[ii], trianglePointIds[jj], trianglePointIds[kk]); + this->PassCellData(id, qeprimal0); + const auto qeprimal1 = output->AddFaceTriangle(edgePointIds[ii], trianglePointIds[kk], trianglePointIds[ii]); + this->PassCellData(id, qeprimal1); +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::SplitTriangleFromTwoEdges( + OutputMeshType * output, + const OutputPointIdentifier * trianglePointIds, + const OutputPointIdentifier * edgePointIds, + const unsigned int * splitEdges, + const InputCellIdentifier id) +{ + unsigned int ii = splitEdges[0]; + unsigned int jj = splitEdges[1]; + + if (ii == 0 && jj == 1) + { + // ii = 0, jj = 1 + const auto qeprimal0 = output->AddFaceTriangle(trianglePointIds[2], trianglePointIds[0], edgePointIds[0]); + this->PassCellData(id, qeprimal0); + const auto qeprimal1 = output->AddFaceTriangle(trianglePointIds[2], edgePointIds[0], edgePointIds[1]); + this->PassCellData(id, qeprimal1); + const auto qeprimal2 = output->AddFaceTriangle(edgePointIds[0], trianglePointIds[1], edgePointIds[1]); + this->PassCellData(id, qeprimal2); + } + else if (ii == 0 && jj == 2) + { + // ii = 0, jj = 2 + const auto qeprimal0 = output->AddFaceTriangle(trianglePointIds[1], trianglePointIds[2], edgePointIds[0]); + this->PassCellData(id, qeprimal0); + const auto qeprimal1 = output->AddFaceTriangle(trianglePointIds[2], edgePointIds[2], edgePointIds[0]); + this->PassCellData(id, qeprimal1); + const auto qeprimal2 = output->AddFaceTriangle(edgePointIds[2], trianglePointIds[0], edgePointIds[0]); + this->PassCellData(id, qeprimal2); + } + else if (ii == 1 && jj == 2) + { + // ii = 1, jj = 2 + const auto qeprimal0 = output->AddFaceTriangle(trianglePointIds[0], trianglePointIds[1], edgePointIds[1]); + this->PassCellData(id, qeprimal0); + const auto qeprimal1 = output->AddFaceTriangle(trianglePointIds[0], edgePointIds[1], edgePointIds[2]); + this->PassCellData(id, qeprimal1); + const auto qeprimal2 = output->AddFaceTriangle(edgePointIds[1], trianglePointIds[2], edgePointIds[2]); + this->PassCellData(id, qeprimal2); + } +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::SplitTriangleFromThreeEdges( + OutputMeshType * output, + const OutputPointIdentifier * trianglePointIds, + const OutputPointIdentifier * edgePointIds, + const InputCellIdentifier id) +{ + // this face was not supposed to be subdivided but all neighbors are + if (this->m_Uniform) + { + const auto qeprimal0 = output->AddFaceTriangle(trianglePointIds[0], edgePointIds[0], edgePointIds[2]); + this->PassCellData(id, qeprimal0); + const auto qeprimal1 = output->AddFaceTriangle(edgePointIds[0], trianglePointIds[1], edgePointIds[1]); + this->PassCellData(id, qeprimal1); + const auto qeprimal2 = output->AddFaceTriangle(edgePointIds[1], trianglePointIds[2], edgePointIds[2]); + this->PassCellData(id, qeprimal2); + const auto qeprimal3 = output->AddFaceTriangle(edgePointIds[0], edgePointIds[1], edgePointIds[2]); + this->PassCellData(id, qeprimal3); + } + else + { + OutputQEType * newTriangleEdge = output->AddFaceTriangle(trianglePointIds[0], edgePointIds[0], edgePointIds[2]); + this->PassCellData(id, newTriangleEdge); + this->m_CellsToBeSubdivided.push_back(newTriangleEdge->GetLeft()); + + newTriangleEdge = output->AddFaceTriangle(edgePointIds[0], trianglePointIds[1], edgePointIds[1]); + this->PassCellData(id, newTriangleEdge); + this->m_CellsToBeSubdivided.push_back(newTriangleEdge->GetLeft()); + + newTriangleEdge = output->AddFaceTriangle(edgePointIds[1], trianglePointIds[2], edgePointIds[2]); + this->PassCellData(id, newTriangleEdge); + this->m_CellsToBeSubdivided.push_back(newTriangleEdge->GetLeft()); + + newTriangleEdge = output->AddFaceTriangle(edgePointIds[0], edgePointIds[1], edgePointIds[2]); + this->PassCellData(id, newTriangleEdge); + this->m_CellsToBeSubdivided.push_back(newTriangleEdge->GetLeft()); + } +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::PassCellData(const InputCellIdentifier inputId, + const OutputQEType * outputEdge) +{ + if (nullptr == outputEdge) + { + return; + } + if (nullptr == this->GetInput()->GetCellData()) + { + return; + } + if (!this->GetInput()->GetCellData()->IndexExists(inputId)) + { + return; + } + const auto data = this->GetInput()->GetCellData()->ElementAt(inputId); + this->GetOutput()->SetCellData(outputEdge->GetLeft(), data); +} + +template +void +TriangleCellSubdivisionQuadEdgeMeshFilter::PrintSelf(std::ostream & os, Indent indent) const +{ + Superclass::PrintSelf(os, indent); + os << indent << "Is Uniform Subdivision: " << m_Uniform << std::endl; +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h new file mode 100644 index 000000000000..8106f255495e --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h @@ -0,0 +1,116 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h +#define itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_h + +#include "itkTriangleCellSubdivisionQuadEdgeMeshFilter.h" + +namespace itk +{ +/** + * \class TriangleEdgeCellSubdivisionQuadEdgeMeshFilter + * + * \brief FIXME + * \ingroup SubdivisionQuadEdgeMeshFilter + */ +template +class TriangleEdgeCellSubdivisionQuadEdgeMeshFilter + : public TriangleCellSubdivisionQuadEdgeMeshFilter +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(TriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + + using Self = TriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using Superclass = TriangleCellSubdivisionQuadEdgeMeshFilter; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + using InputMeshType = typename Superclass::InputMeshType; + using InputMeshPointer = typename Superclass::InputMeshPointer; + using InputMeshConstPointer = typename Superclass::InputMeshConstPointer; + using InputPointsContainer = typename Superclass::InputPointsContainer; + using InputPointsContainerPointer = typename Superclass::InputPointsContainerPointer; + using InputPointsContainerConstIterator = typename Superclass::InputPointsContainerConstIterator; + using InputPointsContainerIterator = typename Superclass::InputPointsContainerIterator; + using InputCellsContainer = typename Superclass::InputCellsContainer; + using InputCellsContainerPointer = typename Superclass::InputCellsContainerPointer; + using InputCellsContainerIterator = typename Superclass::InputCellsContainerIterator; + using InputCellsContainerConstIterator = typename Superclass::InputCellsContainerConstIterator; + using InputPointType = typename Superclass::InputPointType; + using InputCoordType = typename Superclass::InputCoordinateType; + using InputPointIdentifier = typename Superclass::InputPointIdentifier; + using InputCellIdentifier = typename Superclass::InputCellIdentifier; + using InputCellType = typename Superclass::InputCellType; + using InputQEType = typename Superclass::InputQEType; + using InputMeshTraits = typename Superclass::InputMeshTraits; + using InputPointIdIterator = typename Superclass::InputPointIdIterator; + + using OutputMeshType = typename Superclass::OutputMeshType; + using OutputMeshPointer = typename Superclass::OutputMeshPointer; + using OutputPointsContainerPointer = typename Superclass::OutputPointsContainerPointer; + using OutputPointsContainerIterator = typename Superclass::OutputPointsContainerIterator; + using OutputCellsContainer = typename Superclass::OutputCellsContainer; + using OutputCellsContainerPointer = typename Superclass::OutputCellsContainerPointer; + using OutputCellsContainerIterator = typename Superclass::OutputCellsContainerIterator; + using OutputCellsContainerConstIterator = typename Superclass::OutputCellsContainerConstIterator; + using OutputPointType = typename Superclass::OutputPointType; + using OutputCoordType = typename Superclass::OutputCoordinateType; + using OutputPointIdentifier = typename Superclass::OutputPointIdentifier; + using OutputCellIdentifier = typename Superclass::OutputCellIdentifier; + using OutputCellType = typename Superclass::OutputCellType; + using OutputQEType = typename Superclass::OutputQEType; + using OutputMeshTraits = typename Superclass::OutputMeshTraits; + using OutputPointIdIterator = typename Superclass::OutputPointIdIterator; + + using SubdivisionCellContainer = std::list; + using SubdivisionCellContainerConstIterator = typename SubdivisionCellContainer::const_iterator; + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(TriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + itkGetConstReferenceMacro(EdgesToBeSubdivided, SubdivisionCellContainer); + + void + SetCellsToBeSubdivided(const SubdivisionCellContainer & EdgesList); + void + AddSubdividedEdge(InputQEType * edge); + +protected: + TriangleEdgeCellSubdivisionQuadEdgeMeshFilter(); + ~TriangleEdgeCellSubdivisionQuadEdgeMeshFilter() override = default; + + void + AddNewCellPoints(InputCellType * itkNotUsed(cell)) override + {} + virtual void + AddNewEdgePoints(InputQEType * edge) = 0; + void + GenerateOutputPoints() override; + + void + PrintSelf(std::ostream & os, Indent indent) const override; + + SubdivisionCellContainer m_EdgesToBeSubdivided; +}; +} // end namespace itk + +#ifndef ITK_MANUAL_INSTANTIATION +# include "itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx" +#endif + +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx new file mode 100644 index 000000000000..7385a20fd797 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/include/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.hxx @@ -0,0 +1,101 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#ifndef itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx +#define itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilter_hxx + + +namespace itk +{ +template +TriangleEdgeCellSubdivisionQuadEdgeMeshFilter::TriangleEdgeCellSubdivisionQuadEdgeMeshFilter() = default; + +template +void +TriangleEdgeCellSubdivisionQuadEdgeMeshFilter::SetCellsToBeSubdivided( + const SubdivisionCellContainer & EdgesList) +{ + this->m_EdgesToBeSubdivided = EdgesList; + this->Modified(); +} + +template +void +TriangleEdgeCellSubdivisionQuadEdgeMeshFilter::AddSubdividedEdge(InputQEType * edge) +{ + this->m_EdgesToBeSubdivided.push_back(edge); + this->Modified(); +} + +template +void +TriangleEdgeCellSubdivisionQuadEdgeMeshFilter::GenerateOutputPoints() +{ + // 1. Copy points from input to output + this->CopyInputMeshToOutputMeshPoints(); + + // 2. Initialize edgePoints container + this->m_EdgesPointIdentifier->Initialize(); + + this->m_Uniform = this->m_EdgesToBeSubdivided.empty(); + + if (this->m_Uniform) + { + typename InputCellsContainer::ConstPointer edges = this->GetInput()->GetEdgeCells(); + if (!edges) + { + itkExceptionMacro("<Begin(); + while (eter != edges->End()) + { + auto * edge = dynamic_cast(eter.Value()); + if (edge) + { + this->AddNewEdgePoints(edge->GetQEGeom()); + } + ++eter; + } + } + else + { + SubdivisionCellContainerConstIterator it = this->m_EdgesToBeSubdivided.begin(); + SubdivisionCellContainerConstIterator end = this->m_EdgesToBeSubdivided.end(); + + while (it != end) + { + if (*it) + { + this->AddNewEdgePoints(*it); + } + ++it; + } + } +} + +template +void +TriangleEdgeCellSubdivisionQuadEdgeMeshFilter::PrintSelf(std::ostream & os, + Indent indent) const +{ + Superclass::PrintSelf(os, indent); +} +} // namespace itk +#endif diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/itk-module.cmake b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/itk-module.cmake new file mode 100644 index 000000000000..2844f80fd8bd --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/itk-module.cmake @@ -0,0 +1,20 @@ +# the top-level README is used for describing this module, just +# re-used it for documentation here +# itk_module() defines the module dependencies in SubdivisionQuadEdgeMeshFilter +# The testing module in SubdivisionQuadEdgeMeshFilter depends on ITKTestKernel +# By convention those modules outside of ITK are not prefixed with +# ITK + +# define the dependencies of the include module and the tests +itk_module( + SubdivisionQuadEdgeMeshFilter + DEPENDS + ITKQuadEdgeMesh + ITKQuadEdgeMeshFiltering + TEST_DEPENDS + ITKTestKernel + ITKIOMesh + DESCRIPTION "Module ingested from upstream." + EXCLUDE_FROM_DEFAULT + # Not used for header only libraries ENABLE_SHARED +) diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/CMakeLists.txt b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/CMakeLists.txt new file mode 100644 index 000000000000..0a3da4729d6d --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/CMakeLists.txt @@ -0,0 +1,111 @@ +itk_module_test() + +set( + SubdivisionQuadEdgeMeshFilterTests + itkCriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx + itkCriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx + itkSubdivisionQuadEdgeMeshFilterRegressionTest.cxx + itkTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx + itkTriangleCellSubdivisionQuadEdgeMeshFilterCellDataTest.cxx + itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx +) + +createtestdriver(SubdivisionQuadEdgeMeshFilter "${SubdivisionQuadEdgeMeshFilter-Test_LIBRARIES}" "${SubdivisionQuadEdgeMeshFilterTests}") + +set(INPUTDATA ${CMAKE_CURRENT_SOURCE_DIR}/data) +set(TEMP ${ITK_TEST_OUTPUT_DIR}) + +# Test TriangleCellSubdivision Filters + +foreach(METHOD 0 1 2 3) + itk_add_test( + NAME itkCriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest${METHOD} + COMMAND + SubdivisionQuadEdgeMeshFilterTestDriver + itkCriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest + DATA{${INPUTDATA}/venus.vtk} + ${TEMP}/venus_criterion_${METHOD}.vtk + ${METHOD} + 0.01 + ) + + foreach(RESOLUTION 1 2) + itk_add_test( + NAME + itkAdaptiveTriangleCellSubdivisionQuadEdgeMeshFilterTest${METHOD}${RESOLUTION} + COMMAND + SubdivisionQuadEdgeMeshFilterTestDriver + itkTriangleCellSubdivisionQuadEdgeMeshFilterTest + DATA{${INPUTDATA}/venus.vtk} + ${TEMP}/venus_adaptive_${METHOD}${RESOLUTION}.vtk + ${METHOD} + ${RESOLUTION} + ) + + foreach(ADAPTIVE 0 1) + itk_add_test( + NAME + itkAdaptiveTriangleCellSubdivisionQuadEdgeMeshFilterTest${METHOD}${RESOLUTION}${ADAPTIVE} + COMMAND + SubdivisionQuadEdgeMeshFilterTestDriver + itkTriangleCellSubdivisionQuadEdgeMeshFilterTest + DATA{${INPUTDATA}/venus.vtk} + ${TEMP}/venus_adaptive_${METHOD}${RESOLUTION}${ADAPTIVE}.vtk + ${METHOD} + ${RESOLUTION} + ${ADAPTIVE} + ) + endforeach() + endforeach() +endforeach() + +# Test TriangleEdgeCellSubdivision Filters + +foreach(METHOD 0 1 2) + itk_add_test( + NAME itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest${METHOD} + COMMAND + SubdivisionQuadEdgeMeshFilterTestDriver + itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest + DATA{${INPUTDATA}/venus.vtk} + ${TEMP}/venus_edge_${METHOD}.vtk + ${METHOD} + 0.1 + ) + + itk_add_test( + NAME itkCriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest${METHOD} + COMMAND + SubdivisionQuadEdgeMeshFilterTestDriver + itkCriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest + DATA{${INPUTDATA}/venus.vtk} + ${TEMP}/venus_criterion_edge_${METHOD}.vtk + ${METHOD} + 0.05 + ) +endforeach() + +# Test whether CellData is properly passed + +itk_add_test( + NAME itkTriangleCellSubdivisionQuadEdgeMeshFilterCellDataTest + COMMAND + SubdivisionQuadEdgeMeshFilterTestDriver + itkTriangleCellSubdivisionQuadEdgeMeshFilterCellDataTest +) + +# Regressions for the smoothedPointSet cell-id-vs-point-id bug and the +# infinite-loop on non-triangle cells. TIMEOUT guards against a future +# regression that re-introduces the hang. +itk_add_test( + NAME itkSubdivisionQuadEdgeMeshFilterRegressionTest + COMMAND + SubdivisionQuadEdgeMeshFilterTestDriver + itkSubdivisionQuadEdgeMeshFilterRegressionTest +) +set_tests_properties( + itkSubdivisionQuadEdgeMeshFilterRegressionTest + PROPERTIES + TIMEOUT + 30 +) diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/data/venus.vtk.cid b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/data/venus.vtk.cid new file mode 100644 index 000000000000..9c0f6b3c787f --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/data/venus.vtk.cid @@ -0,0 +1 @@ +bafkreid5y4lgjusq3e44rsfh2vixslxbujyyzwqxd6y72jonhr6d2evm5e diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkCriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkCriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx new file mode 100644 index 000000000000..076e537ecee6 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkCriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx @@ -0,0 +1,182 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#include "itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkConditionalSubdivisionQuadEdgeMeshFilter.h" +#include "itkCellAreaTriangleCellSubdivisionCriterion.h" +#include "itkQuadEdgeMeshParamMatrixCoefficients.h" +#include "itkSmoothingQuadEdgeMeshFilter.h" +#include "itkMeshFileReader.h" +#include "itkMeshFileWriter.h" +#include "itkTestingMacros.h" + +template +int +CriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + + using TriangleCellSubdivisionFilterType = TTriangleCellSubdivisionFilter; + using InputMeshType = typename TriangleCellSubdivisionFilterType::InputMeshType; + using OutputMeshType = typename TriangleCellSubdivisionFilterType::OutputMeshType; + + using CriterionType = + itk::CellAreaTriangleCellSubdivisionCriterion; + using ReaderType = itk::MeshFileReader; + using WriterType = itk::MeshFileWriter; + + const auto reader = ReaderType::New(); + reader->SetFileName(argv[1]); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + const auto subdivision = TriangleCellSubdivisionFilterType::New(); + const auto criterion = CriterionType::New(); + ITK_TEST_EXPECT_EQUAL(criterion->GetNameOfClass(), std::string("CellAreaTriangleCellSubdivisionCriterion")); + criterion->SetMaximumArea(1.0); + ITK_TEST_SET_GET_VALUE(1.0, criterion->GetMaximumArea()); + if (argc >= 5) + { + float area = std::atof(argv[4]); + criterion->SetMaximumArea(area); + ITK_TEST_SET_GET_VALUE(area, criterion->GetMaximumArea()); + } + + subdivision->SetSubdivisionCriterion(criterion.GetPointer()); + subdivision->SetInput(reader->GetOutput()); + subdivision->Update(); + const auto output = subdivision->GetOutput(); + + bool smoothing = false; + if (argc >= 6) + { + smoothing = true; + } + + if (smoothing) + { + using OutputMeshSmoothingFilterType = itk::SmoothingQuadEdgeMeshFilter; + using OnesMatrixCoefficientsType = itk::OnesMatrixCoefficients; + + OnesMatrixCoefficientsType coef; + const auto meshSmoothingFilter = OutputMeshSmoothingFilterType::New(); + meshSmoothingFilter->SetInput(output); + meshSmoothingFilter->SetCoefficientsMethod(&coef); + meshSmoothingFilter->SetDelaunayConforming(1); + meshSmoothingFilter->SetNumberOfIterations(1); + ITK_TRY_EXPECT_NO_EXCEPTION(meshSmoothingFilter->Update()); + + output->Graft(meshSmoothingFilter->GetOutput()); + } + + const auto writer = WriterType::New(); + writer->SetFileName(argv[2]); + writer->SetInput(output); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + return EXIT_SUCCESS; +} + +int +itkCriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Missing Parameters " << std::endl; + std::cerr << "Usage: " << argv[0]; + std::cerr << " inputMeshFile outputMeshFile subdivisionType area" << std::endl; + std::cerr << " 0 : ModifiedButterfly " << std::endl; + std::cerr << " 1 : Linear " << std::endl; + std::cerr << " 2 : Loop " << std::endl; + std::cerr << " 3 : Squarethree " << std::endl; + return EXIT_FAILURE; + } + + using MeshPixelType = float; + constexpr unsigned int Dimension = 3; + + using InputMeshType = itk::QuadEdgeMesh; + using OutputMeshType = itk::QuadEdgeMesh; + + using ButterflySubdivisionFilterType = + itk::ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter; + using LinearSubdivisionFilterType = + itk::LinearTriangleCellSubdivisionQuadEdgeMeshFilter; + using LoopSubdivisionFilterType = itk::LoopTriangleCellSubdivisionQuadEdgeMeshFilter; + using SquareSubdivisionFilterType = + itk::SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter; + + using ConditionalButterflySubdivisionFilterType = + itk::ConditionalSubdivisionQuadEdgeMeshFilter; + using ConditionalLinearSubdivisionFilterType = + itk::ConditionalSubdivisionQuadEdgeMeshFilter; + using ConditionalLoopSubdivisionFilterType = + itk::ConditionalSubdivisionQuadEdgeMeshFilter; + using ConditionalSquareSubdivisionFilterType = + itk::ConditionalSubdivisionQuadEdgeMeshFilter; + + if (argc >= 4) + { + int type = std::atoi(argv[3]); + + switch (type) + { + case 0: + { + const auto filter = ConditionalButterflySubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, ConditionalSubdivisionQuadEdgeMeshFilter, QuadEdgeMeshToQuadEdgeMeshFilter); + return CriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, + argv); + } + case 1: + { + const auto filter = ConditionalLinearSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, ConditionalSubdivisionQuadEdgeMeshFilter, QuadEdgeMeshToQuadEdgeMeshFilter); + return CriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, + argv); + } + case 2: + { + const auto filter = ConditionalLoopSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, ConditionalSubdivisionQuadEdgeMeshFilter, QuadEdgeMeshToQuadEdgeMeshFilter); + return CriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + case 3: + { + const auto filter = ConditionalSquareSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, ConditionalSubdivisionQuadEdgeMeshFilter, QuadEdgeMeshToQuadEdgeMeshFilter); + return CriterionTriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, + argv); + } + default: + std::cerr << "Invalid subdivision type : " << type << std::endl; + return EXIT_FAILURE; + } + } + else + { + std::cerr << "You must have subdivision type " << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkCriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkCriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx new file mode 100644 index 000000000000..a27da8939c13 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkCriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx @@ -0,0 +1,170 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#include "itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkConditionalSubdivisionQuadEdgeMeshFilter.h" +#include "itkEdgeLengthTriangleEdgeCellSubdivisionCriterion.h" +#include "itkQuadEdgeMeshParamMatrixCoefficients.h" +#include "itkSmoothingQuadEdgeMeshFilter.h" +#include "itkMeshFileReader.h" +#include "itkMeshFileWriter.h" +#include "itkTestingMacros.h" + +template +int +CriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + + using TriangleEdgeCellSubdivisionFilterType = TTriangleEdgeCellSubdivisionFilter; + using InputMeshType = typename TriangleEdgeCellSubdivisionFilterType::InputMeshType; + using OutputMeshType = typename TriangleEdgeCellSubdivisionFilterType::OutputMeshType; + + using CriterionType = itk::EdgeLengthTriangleEdgeCellSubdivisionCriterion< + typename TriangleEdgeCellSubdivisionFilterType::SubdivisionFilterType>; + using ReaderType = itk::MeshFileReader; + using WriterType = itk::MeshFileWriter; + + const auto reader = ReaderType::New(); + reader->SetFileName(argv[1]); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + const auto subdivision = TriangleEdgeCellSubdivisionFilterType::New(); + const auto criterion = CriterionType::New(); + ITK_TEST_EXPECT_EQUAL(criterion->GetNameOfClass(), std::string("EdgeLengthTriangleEdgeCellSubdivisionCriterion")); + criterion->SetMaximumLength(1.0); + ITK_TEST_SET_GET_VALUE(1.0, criterion->GetMaximumLength()); + if (argc >= 5) + { + float length = std::atof(argv[4]); + criterion->SetMaximumLength(length); + ITK_TEST_SET_GET_VALUE(length, criterion->GetMaximumLength()); + } + + subdivision->SetSubdivisionCriterion(criterion.GetPointer()); + subdivision->SetInput(reader->GetOutput()); + subdivision->Update(); + const auto output = subdivision->GetOutput(); + + bool smoothing = false; + if (argc >= 6) + { + smoothing = true; + } + + if (smoothing) + { + using OutputMeshSmoothingFilterType = itk::SmoothingQuadEdgeMeshFilter; + using OnesMatrixCoefficientsType = itk::OnesMatrixCoefficients; + + OnesMatrixCoefficientsType coef; + const auto meshSmoothingFilter = OutputMeshSmoothingFilterType::New(); + meshSmoothingFilter->SetInput(output); + meshSmoothingFilter->SetCoefficientsMethod(&coef); + meshSmoothingFilter->SetDelaunayConforming(1); + meshSmoothingFilter->SetNumberOfIterations(1); + ITK_TRY_EXPECT_NO_EXCEPTION(meshSmoothingFilter->Update()); + + output->Graft(meshSmoothingFilter->GetOutput()); + } + + const auto writer = WriterType::New(); + writer->SetFileName(argv[2]); + writer->SetInput(output); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + return EXIT_SUCCESS; +} + +int +itkCriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Missing Parameters " << std::endl; + std::cerr << "Usage: " << argv[0]; + std::cerr << " inputMeshFile outputMeshFile subdivisionType area" << std::endl; + std::cerr << " 0 : ModifiedButterfly " << std::endl; + std::cerr << " 1 : Linear " << std::endl; + std::cerr << " 2 : Loop " << std::endl; + return EXIT_FAILURE; + } + + using MeshPixelType = float; + constexpr unsigned int Dimension = 3; + + using InputMeshType = itk::QuadEdgeMesh; + using OutputMeshType = itk::QuadEdgeMesh; + + using ButterflySubdivisionFilterType = + itk::ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using LinearSubdivisionFilterType = + itk::LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using LoopSubdivisionFilterType = + itk::LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + + using ConditionalButterflySubdivisionFilterType = + itk::ConditionalSubdivisionQuadEdgeMeshFilter; + using ConditionalLinearSubdivisionFilterType = + itk::ConditionalSubdivisionQuadEdgeMeshFilter; + using ConditionalLoopSubdivisionFilterType = + itk::ConditionalSubdivisionQuadEdgeMeshFilter; + + if (argc >= 4) + { + int type = std::atoi(argv[3]); + + switch (type) + { + case 0: + { + const auto filter = ConditionalButterflySubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, ConditionalSubdivisionQuadEdgeMeshFilter, QuadEdgeMeshToQuadEdgeMeshFilter); + return CriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest( + argc, argv); + } + case 1: + { + const auto filter = ConditionalLinearSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, ConditionalSubdivisionQuadEdgeMeshFilter, QuadEdgeMeshToQuadEdgeMeshFilter); + return CriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(argc, + argv); + } + case 2: + { + const auto filter = ConditionalLoopSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, ConditionalSubdivisionQuadEdgeMeshFilter, QuadEdgeMeshToQuadEdgeMeshFilter); + return CriterionTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(argc, + argv); + } + default: + std::cerr << "Invalid subdivision type : " << type << std::endl; + return EXIT_FAILURE; + } + } + else + { + std::cerr << "You must have subdivision type " << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkSubdivisionQuadEdgeMeshFilterRegressionTest.cxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkSubdivisionQuadEdgeMeshFilterRegressionTest.cxx new file mode 100644 index 000000000000..52f8b2a75e4c --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkSubdivisionQuadEdgeMeshFilterRegressionTest.cxx @@ -0,0 +1,189 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +// Regressions for two bugs in the upstream ITKSubdivisionQuadEdgeMeshFilter +// module that the existing venus.vtk-driven tests did not exercise: +// +// 1. LoopTriangleCellSubdivisionQuadEdgeMeshFilter inserted cell IDs into +// its smoothedPointSet rather than point IDs, so the non-uniform +// adaptive Loop path silently skipped smoothing for nearly every +// vertex it should have moved. +// +// 2. TriangleCellSubdivisionQuadEdgeMeshFilter::GenerateOutputCells() hung +// on any input mesh containing a non-triangle cell — the cell iterator +// was not advanced before the guard-branch `continue`. The same bug +// also lived in SquareThreeTriangleCellSubdivision's override, but +// that override's AddNewCellPoints throws on non-triangle input first, +// so the hang was only reachable in the parent class via Loop's +// non-uniform path (Loop does not override GenerateOutputCells). +// +// The existing venus.vtk regression tests cannot trigger either bug because +// venus.vtk is purely triangular AND the existing tests exercise only the +// uniform-subdivide-all path. + +#include "itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkQuadEdgeMesh.h" +#include "itkQuadEdgeMeshPolygonCell.h" + +#include + + +namespace +{ + +using MeshType = itk::QuadEdgeMesh; +using PolygonCellType = itk::QuadEdgeMeshPolygonCell; + + +// Build a mesh with two triangles sharing edge 1-2: +// +// Points: 0 (0,0,0), 1 (1,0,0), 2 (0,1,0), 3 (1,1,0) +// Cells: triangle (0,1,2), triangle (1,3,2) +MeshType::Pointer +BuildTwoTriangleMesh() +{ + const auto mesh = MeshType::New(); + + MeshType::PointType p; + p[2] = 0.0; + p[0] = 0.0; + p[1] = 0.0; + mesh->SetPoint(0, p); + p[0] = 1.0; + p[1] = 0.0; + mesh->SetPoint(1, p); + p[0] = 0.0; + p[1] = 1.0; + mesh->SetPoint(2, p); + p[0] = 1.0; + p[1] = 1.0; + mesh->SetPoint(3, p); + + mesh->AddFaceTriangle(0, 1, 2); + mesh->AddFaceTriangle(1, 3, 2); + return mesh; +} + + +// Inject a non-triangle (4-point) polygon cell into the cells container at +// an unused ID, bypassing AddFace's geometric validation. This produces +// exactly the shape the bug requires: GenerateOutputCells() iterates the +// cells container and must skip the non-triangle without spinning. +void +InjectFourPointPolygonCell(const MeshType::Pointer & mesh, MeshType::CellIdentifier id) +{ + auto * poly = new PolygonCellType(4); + poly->SetPointId(0, 0); + poly->SetPointId(1, 1); + poly->SetPointId(2, 3); + poly->SetPointId(3, 2); + + MeshType::CellAutoPointer ap; + ap.TakeOwnership(poly); + mesh->GetCells()->InsertElement(id, ap.ReleaseOwnership()); +} + + +// Returns true if `ipt` and `opt` differ in any coordinate. +bool +PointMoved(const MeshType::PointType & ipt, const MeshType::PointType & opt) +{ + for (unsigned int k = 0; k < MeshType::PointDimension; ++k) + { + if (ipt[k] != opt[k]) + { + return true; + } + } + return false; +} + +} // namespace + + +int +itkSubdivisionQuadEdgeMeshFilterRegressionTest(int, char *[]) +{ + bool ok = true; + + // ---- Bug 2: GenerateOutputCells must terminate on a mesh with a + // non-triangle cell. Use Loop in non-uniform mode and list only the two + // triangle cell IDs in CellsToBeSubdivided so AddNewCellPoints is not + // called on the non-triangle (which would throw). GenerateOutputCells + // still iterates ALL cells, which is exactly the bug path. The CTest + // TIMEOUT of 30 seconds catches a regression that re-introduces the hang. + { + const auto mesh = BuildTwoTriangleMesh(); + InjectFourPointPolygonCell(mesh, 100); + + using FilterType = itk::LoopTriangleCellSubdivisionQuadEdgeMeshFilter; + const auto filter = FilterType::New(); + + typename FilterType::SubdivisionCellContainer cellsToSubdivide; + cellsToSubdivide.push_back(0); + cellsToSubdivide.push_back(1); + filter->SetCellsToBeSubdivided(cellsToSubdivide); + filter->SetInput(mesh); + filter->Update(); + + std::cout << "Loop terminated on mixed-cell input." << std::endl; + } + + // ---- Bug 1: Loop non-uniform adaptive path must smooth ALL points of the + // listed cells, not just those whose point-index coincidentally matches a + // cell-index. With CellsToBeSubdivided = {0}, cell 0 has point IDs + // {0, 1, 2}; the smoothing set must therefore contain {0, 1, 2} and the + // output positions of points 0, 1, and 2 must all move. Before the fix, + // smoothedPointSet contained {0} (the cell ID), so only point 0 ever moved. + { + const auto mesh = BuildTwoTriangleMesh(); + + using FilterType = itk::LoopTriangleCellSubdivisionQuadEdgeMeshFilter; + const auto filter = FilterType::New(); + typename FilterType::SubdivisionCellContainer cellsToSubdivide; + cellsToSubdivide.push_back(0); + filter->SetCellsToBeSubdivided(cellsToSubdivide); + filter->SetInput(mesh); + filter->Update(); + + const auto out = filter->GetOutput(); + + unsigned int nMoved = 0; + for (MeshType::PointIdentifier pid = 0; pid < 4; ++pid) + { + MeshType::PointType ipt; + MeshType::PointType opt; + mesh->GetPoint(pid, &ipt); + out->GetPoint(pid, &opt); + if (PointMoved(ipt, opt)) + { + ++nMoved; + } + } + std::cout << "Loop non-uniform: " << nMoved << " of 4 points moved." << std::endl; + if (nMoved < 3) + { + std::cerr << "FAIL: expected at least 3 of {0,1,2} to be smoothed (cell 0's vertices); " + << "got " << nMoved << ". This is the smoothedPointSet cell-id-vs-point-id " + << "regression." << std::endl; + ok = false; + } + } + + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleCellSubdivisionQuadEdgeMeshFilterCellDataTest.cxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleCellSubdivisionQuadEdgeMeshFilterCellDataTest.cxx new file mode 100644 index 000000000000..75379d56e225 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleCellSubdivisionQuadEdgeMeshFilterCellDataTest.cxx @@ -0,0 +1,150 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +// ITK +#include +#include +#include +#include +#include +#include +#include +#include + +template +void +SubdivisionTestHelper(const bool uniform) +{ + + // Typedefs + const unsigned int Dimension = 3; + using TCoordinate = float; + using TQEMesh = itk::QuadEdgeMesh; + using TSource = itk::RegularSphereMeshSource; + using TTriangleHelper = itk::TriangleHelper; + using TSubdivision = SubdivisionType; + using TSubdivisionIt = itk::IterativeTriangleCellSubdivisionQuadEdgeMeshFilter; + + // Generate some input mesh data + const auto sphere = TSource::New(); + sphere->Update(); + + const auto i_mesh = TQEMesh::New(); + i_mesh->Graft(sphere->GetOutput()); + i_mesh->DisconnectPipeline(); + + typename TSubdivisionIt::SubdivisionCellContainer cells_to_subdivide; + + // Assign cell data (different for each octant). + for (auto it = i_mesh->GetCells()->Begin(); it != i_mesh->GetCells()->End(); ++it) + { + const auto cell = it.Value(); + + const auto centroid = TTriangleHelper::ComputeGravityCenter(i_mesh->GetPoint(cell->GetPointIds()[0]), + i_mesh->GetPoint(cell->GetPointIds()[1]), + i_mesh->GetPoint(cell->GetPointIds()[2])); + + unsigned int data = 0; + if (centroid[0] < 0 && centroid[1] < 0 && centroid[2] < 0) + { + data = 1; + } + else if (centroid[0] < 0 && centroid[1] < 0 && centroid[2] >= 0) + { + data = 2; + } + else if (centroid[0] < 0 && centroid[1] >= 0 && centroid[2] < 0) + { + data = 3; + } + else if (centroid[0] < 0 && centroid[1] >= 0 && centroid[2] >= 0) + { + data = 4; + cells_to_subdivide.push_back(it.Index()); + } + else if (centroid[0] >= 0 && centroid[1] < 0 && centroid[2] < 0) + { + data = 5; + } + else if (centroid[0] >= 0 && centroid[1] < 0 && centroid[2] >= 0) + { + data = 6; + } + else if (centroid[0] >= 0 && centroid[1] >= 0 && centroid[2] < 0) + { + data = 7; + } + else if (centroid[0] >= 0 && centroid[1] >= 0 && centroid[2] >= 0) + { + data = 8; + } + + i_mesh->SetCellData(it.Index(), data); + } + + // Assert one CellData entry for each Cell + const auto i_cell = i_mesh->GetNumberOfCells(); + const auto i_data = i_mesh->GetCellData()->Size(); + itkAssertOrThrowMacro(i_cell == i_data, "Incorrect number of entries in input cell data array."); + + const size_t resolution = 3; + + const auto subdivide = TSubdivisionIt::New(); + subdivide->SetResolutionLevels(resolution); + if (!uniform) + { + subdivide->SetCellsToBeSubdivided(cells_to_subdivide); + } + subdivide->SetInput(i_mesh); + subdivide->Update(); + + const auto o_mesh = TQEMesh::New(); + o_mesh->Graft(subdivide->GetOutput()); + o_mesh->DisconnectPipeline(); + + // Assert one CellData entry for each Cell + const auto o_cell = o_mesh->GetNumberOfCells(); + const auto o_data = o_mesh->GetCellData()->Size(); + itkAssertOrThrowMacro(o_cell == o_data, "Incorrect number of entries in output cell data array."); +} + +int +itkTriangleCellSubdivisionQuadEdgeMeshFilterCellDataTest(int, char *[]) +{ + + const unsigned int Dimension = 3; + using TCoordinate = float; + using TQEMesh = itk::QuadEdgeMesh; + + using TButterfly = itk::ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter; + using TLinear = itk::LinearTriangleCellSubdivisionQuadEdgeMeshFilter; + using TLoop = itk::LoopTriangleCellSubdivisionQuadEdgeMeshFilter; + using TSquare = itk::SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter; + + SubdivisionTestHelper(true); + SubdivisionTestHelper(true); + SubdivisionTestHelper(true); + SubdivisionTestHelper(true); + + SubdivisionTestHelper(false); + SubdivisionTestHelper(false); + SubdivisionTestHelper(false); + SubdivisionTestHelper(false); + + return EXIT_SUCCESS; +} diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx new file mode 100644 index 000000000000..a306ec81d2e3 --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleCellSubdivisionQuadEdgeMeshFilterTest.cxx @@ -0,0 +1,214 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#include "itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLinearTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLoopTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkIterativeTriangleCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkQuadEdgeMesh.h" +#include "itkQuadEdgeMeshParamMatrixCoefficients.h" +#include "itkSmoothingQuadEdgeMeshFilter.h" +#include "itkMeshFileReader.h" +#include "itkMeshFileWriter.h" +#include "itkTestingMacros.h" + +template +int +TriangleCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + using TriangleCellSubdivisionFilterType = TTriangleCellSubdivisionFilter; + + using InputMeshType = typename TriangleCellSubdivisionFilterType::InputMeshType; + using OutputMeshType = typename TriangleCellSubdivisionFilterType::OutputMeshType; + + using IterativeTriangleCellSubdivisionFilterType = + itk::IterativeTriangleCellSubdivisionQuadEdgeMeshFilter; + + using ReaderType = itk::MeshFileReader; + using WriterType = itk::MeshFileWriter; + + const auto reader = ReaderType::New(); + reader->SetFileName(argv[1]); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + const auto subdivision = IterativeTriangleCellSubdivisionFilterType::New(); + ITK_TEST_EXPECT_EQUAL(subdivision->GetNameOfClass(), + std::string("IterativeTriangleCellSubdivisionQuadEdgeMeshFilter")); + subdivision->Print(std::cout); + + if (argc >= 5) + { + unsigned int n = std::atoi(argv[4]); + subdivision->SetResolutionLevels(n); + ITK_TEST_SET_GET_VALUE(n, subdivision->GetResolutionLevels()); + } + + if (argc >= 6) + { + int type = std::atoi(argv[5]); + if (type) + { + typename IterativeTriangleCellSubdivisionFilterType::SubdivisionCellContainer cellsToBeSubdivided; + + cellsToBeSubdivided.push_back(0); + cellsToBeSubdivided.push_back(1); + cellsToBeSubdivided.push_back(2); + cellsToBeSubdivided.push_back(3); + cellsToBeSubdivided.push_back(5); + cellsToBeSubdivided.push_back(6); + cellsToBeSubdivided.push_back(9); + + subdivision->SetCellsToBeSubdivided(cellsToBeSubdivided); + + ITK_TEST_EXPECT_EQUAL(7, subdivision->GetCellsToBeSubdivided().size()); + } + else + { + subdivision->AddSubdividedCellId(0); + subdivision->AddSubdividedCellId(1); + subdivision->AddSubdividedCellId(2); + subdivision->AddSubdividedCellId(3); + subdivision->AddSubdividedCellId(5); + subdivision->AddSubdividedCellId(6); + subdivision->AddSubdividedCellId(9); + + ITK_TEST_EXPECT_EQUAL(7, subdivision->GetCellsToBeSubdivided().size()); + } + } + + subdivision->SetInput(reader->GetOutput()); + ITK_TRY_EXPECT_NO_EXCEPTION(subdivision->Update()); + const auto output = subdivision->GetOutput(); + + bool smoothing = true; + if (argc >= 7) + { + smoothing = false; + } + + if (smoothing) + { + using OutputMeshSmoothingFilterType = itk::SmoothingQuadEdgeMeshFilter; + using OnesMatrixCoefficientsType = itk::OnesMatrixCoefficients; + + OnesMatrixCoefficientsType coef; + const auto meshSmoothingFilter = OutputMeshSmoothingFilterType::New(); + meshSmoothingFilter->SetInput(output); + meshSmoothingFilter->SetCoefficientsMethod(&coef); + // FIXME: Smoothing with the Delaunay Conforming filter causes the following three test failures. + // Temporarily disabling the DC smoothing filter while investigating the cause. + // 3 - itkModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilterTest1 (SEGFAULT) + // 8 - itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilterTest0 (SEGFAULT) + // 9 - itkSquareThreeTriangleCellSubdivisionQuadEdgeMeshFilterTest1 (SEGFAULT) + // meshSmoothingFilter->SetDelaunayConforming(1); + meshSmoothingFilter->SetNumberOfIterations(1); + ITK_TRY_EXPECT_NO_EXCEPTION(meshSmoothingFilter->Update()); + + output->Graft(meshSmoothingFilter->GetOutput()); + } + + const auto writer = WriterType::New(); + writer->SetFileName(argv[2]); + writer->SetInput(output); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + return EXIT_SUCCESS; +} + +int +itkTriangleCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + if (argc < 4) + { + std::cerr << "Error: Missing Parameters!" << std::endl; + std::cerr << "Usage: " << argv[0] << '\n'; + std::cerr << " inputMeshFile\n"; + std::cerr << " outputMeshFile\n"; + std::cerr << " subdivisionType\n"; + std::cerr << " 0 : ModifiedButterfly\n"; + std::cerr << " 1 : Linear\n"; + std::cerr << " 2 : Loop\n"; + std::cerr << " 3 : SquareThree\n"; + std::cerr << " [Resolution]\n"; + std::cerr << " [Adaptive]\n"; + std::cerr << " 0 : AddSubdividedCellId\n"; + std::cerr << " 1 : SetCellsToBeSubdivided\n"; + std::cerr << std::flush; + return EXIT_FAILURE; + } + + using MeshPixelType = float; + constexpr unsigned int Dimension = 3; + + using MeshType = itk::QuadEdgeMesh; + + using ModifiedButterflySubdivisionFilterType = + itk::ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter; + using LinearSubdivisionFilterType = itk::LinearTriangleCellSubdivisionQuadEdgeMeshFilter; + using LoopSubdivisionFilterType = itk::LoopTriangleCellSubdivisionQuadEdgeMeshFilter; + using SquareThreeSubdivisionFilterType = + itk::SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter; + + if (argc >= 4) + { + int type = std::atoi(argv[3]); + + switch (type) + { + case 0: + { + const auto filter = ModifiedButterflySubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS(filter, + ModifiedButterflyTriangleCellSubdivisionQuadEdgeMeshFilter, + TriangleCellSubdivisionQuadEdgeMeshFilter); + return TriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + case 1: + { + const auto filter = LinearSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, LinearTriangleCellSubdivisionQuadEdgeMeshFilter, TriangleCellSubdivisionQuadEdgeMeshFilter); + return TriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + case 2: + { + const auto filter = LoopSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, LoopTriangleCellSubdivisionQuadEdgeMeshFilter, TriangleCellSubdivisionQuadEdgeMeshFilter); + return TriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + case 3: + { + const auto filter = SquareThreeSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, SquareThreeTriangleCellSubdivisionQuadEdgeMeshFilter, TriangleCellSubdivisionQuadEdgeMeshFilter); + return TriangleCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + default: + std::cerr << "Invalid subdivision type : " << type << std::endl; + return EXIT_FAILURE; + } + } + else + { + std::cerr << "You must have subdivision type " << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx new file mode 100644 index 000000000000..6fcf9e137c9a --- /dev/null +++ b/Modules/Filtering/SubdivisionQuadEdgeMeshFilter/test/itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest.cxx @@ -0,0 +1,178 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * 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. + * + *=========================================================================*/ + +#include "itkModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkLoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter.h" +#include "itkQuadEdgeMesh.h" +#include "itkQuadEdgeMeshParamMatrixCoefficients.h" +#include "itkSmoothingQuadEdgeMeshFilter.h" +#include "itkMeshFileReader.h" +#include "itkMeshFileWriter.h" +#include "itkTestingMacros.h" + +template +int +TriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + + using TriangleEdgeCellSubdivisionFilterType = TTriangleEdgeCellSubdivisionFilter; + using InputMeshType = typename TriangleEdgeCellSubdivisionFilterType::InputMeshType; + using OutputMeshType = typename TriangleEdgeCellSubdivisionFilterType::OutputMeshType; + using SubdivisionCellContainer = typename TriangleEdgeCellSubdivisionFilterType::SubdivisionCellContainer; + + using ReaderType = itk::MeshFileReader; + using WriterType = itk::MeshFileWriter; + + const auto reader = ReaderType::New(); + reader->SetFileName(argv[1]); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + const auto subdivision = TriangleEdgeCellSubdivisionFilterType::New(); + + double edgeLengthThreshold = 1.0; + if (argc >= 5) + { + edgeLengthThreshold = std::atof(argv[4]); + } + + SubdivisionCellContainer edgesToBeSubdivided; + typename InputMeshType::PointType pointArray[2]; + const auto input = reader->GetOutput(); + const auto edges = input->GetEdgeCells(); + for (auto eter = edges->Begin(); eter != edges->End(); ++eter) + { + auto * edge = dynamic_cast(eter.Value()); + if (edge) + { + input->GetPoint(edge->PointIdsBegin()[0], &pointArray[0]); + input->GetPoint(edge->PointIdsBegin()[1], &pointArray[1]); + const auto distance = static_cast(pointArray[1].SquaredEuclideanDistanceTo(pointArray[0])); + if (distance > edgeLengthThreshold) + { + std::cout << "to be subdivided edge id = " << eter->Index() << std::endl; + edgesToBeSubdivided.push_back(input->FindEdge(edge->PointIdsBegin()[0], edge->PointIdsBegin()[1])); + } + } + } + + std::cout << "number of subdivided edges = " << edgesToBeSubdivided.size() << std::endl; + subdivision->SetCellsToBeSubdivided(edgesToBeSubdivided); + subdivision->SetInput(reader->GetOutput()); + ITK_TRY_EXPECT_NO_EXCEPTION(subdivision->Update()); + + bool smoothing = true; + if (argc >= 6) + { + smoothing = false; + } + + if (smoothing) + { + using OutputMeshSmoothingFilterType = itk::SmoothingQuadEdgeMeshFilter; + using OnesMatrixCoefficientsType = itk::OnesMatrixCoefficients; + + OnesMatrixCoefficientsType coef; + const auto meshSmoothingFilter = OutputMeshSmoothingFilterType::New(); + meshSmoothingFilter->SetInput(subdivision->GetOutput()); + meshSmoothingFilter->SetCoefficientsMethod(&coef); + // FIXME: Smoothing with the Delaunay Conforming filter causes the following three test failures. + // Temporarily disabling the DC smoothing filter while investigating the cause. + // 23 - itkLinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest (SEGFAULT) + // meshSmoothingFilter->SetDelaunayConforming(1); + meshSmoothingFilter->SetNumberOfIterations(1); + ITK_TRY_EXPECT_NO_EXCEPTION(meshSmoothingFilter->Update()); + + subdivision->GetOutput()->Graft(meshSmoothingFilter->GetOutput()); + } + + const auto writer = WriterType::New(); + writer->SetFileName(argv[2]); + writer->SetInput(subdivision->GetOutput()); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + return EXIT_SUCCESS; +} + +int +itkTriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Missing Parameters " << std::endl; + std::cerr << "Usage: " << argv[0]; + std::cerr << " inputMeshFile outputMeshFile subdivisionType edgeLengthThreshold" << std::endl; + std::cerr << " 0 : ModifiedButterfly " << std::endl; + std::cerr << " 1 : Linear " << std::endl; + std::cerr << " 2 : Loop " << std::endl; + return EXIT_FAILURE; + } + + using MeshPixelType = float; + constexpr unsigned int Dimension = 3; + + using InputMeshType = itk::QuadEdgeMesh; + using OutputMeshType = itk::QuadEdgeMesh; + + using ModifiedButterflySubdivisionFilterType = + itk::ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using LinearSubdivisionFilterType = + itk::LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + using LoopSubdivisionFilterType = + itk::LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter; + + if (argc >= 4) + { + int type = std::atoi(argv[3]); + + switch (type) + { + case 0: + { + const auto filter = ModifiedButterflySubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS(filter, + ModifiedButterflyTriangleEdgeCellSubdivisionQuadEdgeMeshFilter, + TriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + return TriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + case 1: + { + const auto filter = LinearSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, LinearTriangleEdgeCellSubdivisionQuadEdgeMeshFilter, TriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + return TriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + case 2: + { + const auto filter = LoopSubdivisionFilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS( + filter, LoopTriangleEdgeCellSubdivisionQuadEdgeMeshFilter, TriangleEdgeCellSubdivisionQuadEdgeMeshFilter); + return TriangleEdgeCellSubdivisionQuadEdgeMeshFilterTest(argc, argv); + } + default: + std::cerr << "Invalid subdivision type : " << type << std::endl; + return EXIT_FAILURE; + } + } + else + { + std::cerr << "You must have subdivision type " << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/Modules/Remote/SubdivisionQuadEdgeMeshFilter.remote.cmake b/Modules/Remote/SubdivisionQuadEdgeMeshFilter.remote.cmake deleted file mode 100644 index cb77ef6ae0ce..000000000000 --- a/Modules/Remote/SubdivisionQuadEdgeMeshFilter.remote.cmake +++ /dev/null @@ -1,55 +0,0 @@ -#-- # Grading Level Criteria Report -#-- EVALUATION DATE: 2020-03-01 -#-- EVALUATORS: [<>,<>] -#-- -#-- ## Compliance level 5 star (AKA ITK main modules, or remote modules that could become core modules) -#-- - [ ] Widespread community dependance -#-- - [ ] Above 90% code coverage -#-- - [ ] CI dashboards and testing monitored rigorously -#-- - [ ] Key API features are exposed in wrapping interface -#-- - [ ] All requirements of Levels 4,3,2,1 -#-- -#-- ## Compliance Level 4 star (Very high-quality code, perhaps small community dependance) -#-- - [ ] Meets all ITK code style standards -#-- - [ ] No external requirements beyond those needed by ITK proper -#-- - [ ] Builds and passes tests on all supported platforms within 1 month of each core tagged release -#-- - [ ] Windows Shared Library Build with Visual Studio -#-- - [ ] Mac with clang compiller -#-- - [ ] Linux with gcc compiler -#-- - [ ] Active developer community dedicated to maintaining code-base -#-- - [ ] 75% code coverage demonstrated for testing suite -#-- - [ ] Continuous integration testing performed -#-- - [ ] All requirements of Levels 3,2,1 -#-- -#-- ## Compliance Level 3 star (Quality beta code) -#-- - [ ] API | executable interface is considered mostly stable and feature complete -#-- - [ ] 10% C0-code coverage demonstrated for testing suite -#-- - [ ] Some tests exist and pass on at least some platform -#-- - [X] All requirements of Levels 2,1 -#-- -#-- ## Compliance Level 2 star (Alpha code feature API development or niche community/execution environment dependance ) -#-- - [X] Compiles for at least 1 niche set of execution envirionments, and perhaps others -#-- (may depend on specific external tools like a java environment, or specific external libraries to work ) -#-- - [X] All requirements of Levels 1 -#-- -#-- ## Compliance Level 1 star (Pre-alpha features under development and code of unknown quality) -#-- - [X] Code complies on at least 1 platform -#-- -#-- ## Compliance Level 0 star ( Code/Feature of known poor-quality or deprecated status ) -#-- - [ ] Code reviewed and explicitly identified as not recommended for use -#-- -#-- ### Please document here any justification for the criteria above -# Code style enforced by clang-format on 2020-02-19, and clang-tidy modernizations completed - -itk_fetch_module( - SubdivisionQuadEdgeMeshFilter - "Triangle and edge based QuadEdgeMesh subdivision filters. - -See the following Insight Journal's publication: - - Wanlin Zhu, \"Triangle Mesh Subdivision\" - https://doi.org/10.54294/vw0ke0" - MODULE_COMPLIANCE_LEVEL 2 - GIT_REPOSITORY https://github.com/InsightSoftwareConsortium/itkSubdivisionQuadEdgeMeshFilter - GIT_TAG 5c5fecff87d41d3927fa6f67b3b53e0e917fc614 - ) diff --git a/Utilities/Maintenance/RemoteModuleIngest/INGESTION_STRATEGY_v4.md b/Utilities/Maintenance/RemoteModuleIngest/INGESTION_STRATEGY_v4.md new file mode 100644 index 000000000000..2123c8fb686e --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/INGESTION_STRATEGY_v4.md @@ -0,0 +1,321 @@ +# Remote Module Ingestion Strategy — v4 (two-phase split) + +This is the next iteration of the ingestion process. v3 (see +`INGESTION_STRATEGY.md`) folded the upstream-archival step into the +ingestion driver, which made the ingest script harder to re-run and +coupled an irreversible upstream operation (publishing a deletion +commit + flipping `archived=true`) to a reversible local one (rewriting +history into ITK). + +**v4 splits these into two independent phases**, with separate scripts +and separate operator gates between them. + +## TL;DR + +| Phase | Script | Local-clone fate | Upstream side-effects | +|---|---|---|---| +| **A — Ingestion** | `ingest-module-v4.sh` | **Throwaway** — destroy after PR merges | None (no pushes to upstream remote) | +| **B — Upstream archival** | `archive-remote-module.sh` | **Fresh clone** — built from scratch | Pushes one removal commit + promotes `MIGRATION_README.md` to `README.md` + flips `archived=true` | + +The phases share **only one persistent artifact** between them: the +**whitelist** that defines what files migrated to ITK vs. what files +remain in the upstream archive. Every other piece of state is +reproducible. + +## Why two phases + +| v3 problem | v4 solution | +|---|---| +| Ingest script orchestrated upstream archival as a bonus step → re-running the ingest after a CI failure risked re-touching upstream | Phase A never touches the upstream remote at all; failures are 100% local | +| Per-commit pre-commit + ghostflow validity was best-effort; some historical commits failed style hooks during reviewer testing | Phase A applies clang-format + black + commit-prefix sanitization to **every** commit, so every commit independently passes ITK's gates | +| Upstream archival was a manual checklist (per `feedback_ingest_archive_readme.md`) → easy to forget steps, ordering errors | Phase B is a single script with deterministic ordering and post-conditions verifiable via `gh api .../readme` | +| `git blame` on imported history showed mixed formatting from various upstream eras → noise on subsequent maintenance work | Phase A formats every blob across history with the destination ITK's `.clang-format` so blame is uniform | + +## Phase A — Ingestion (local, throwaway) + +### Operator command + +```bash +ingest-module-v4.sh [--dry-run] [--keep-tempdir] +``` + +Examples: + +```bash +ingest-module-v4.sh IOMeshSTL IO # Modules/IO/MeshSTL/ +ingest-module-v4.sh Cuberille Filtering # Modules/Filtering/Cuberille/ +``` + +### What Phase A does + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Pre-flight │ +│ - git-filter-repo, clang-format, black available │ +│ - clean ITK working tree on a feature branch │ +│ - whitelist file readable │ +│ - destination directory does not collide │ +├─────────────────────────────────────────────────────────────────┤ +│ 2. Mirror-clone upstream into ${TMPDIR}/-mirror │ +│ - throwaway location; deleted on success unless --keep- │ +├─────────────────────────────────────────────────────────────────┤ +│ 3. filter-repo: paths-from-file , --tag-rename '' │ +│ - keep only whitelisted files across full history │ +│ - non-whitelisted files vanish from every commit │ +├─────────────────────────────────────────────────────────────────┤ +│ 4. filter-repo: subdirectory move into Modules// │ +├─────────────────────────────────────────────────────────────────┤ +│ 5. sanitize-history.py: │ +│ - For every commit: │ +│ a. Walk file_changes, rewrite each .cxx/.h/.hxx/... blob │ +│ through `clang-format -style=file` (ITK's .clang-format)│ +│ b. Walk file_changes, rewrite each .py blob through `black`│ +│ c. Examine commit subject; if no BUG:/COMP:/DOC:/ENH:/ │ +│ PERF:/STYLE: prefix → auto-prepend a heuristic prefix │ +│ d. Log every prefix decision to │ +│ ${LOG_DIR}/commit-prefix-log.txt │ +├─────────────────────────────────────────────────────────────────┤ +│ 6. Topology verification │ +│ - require ≥1 merge in upstream/main..HEAD if upstream had any│ +│ - reject linearization (per feedback_ingest_merge_topology) │ +├─────────────────────────────────────────────────────────────────┤ +│ 7. Mode-A merge into ITK feature branch │ +│ - --no-ff --allow-unrelated-histories │ +│ - merge commit subject: "ENH: Ingest remote module" │ +├─────────────────────────────────────────────────────────────────┤ +│ 8. Verification │ +│ - pre-commit run --all-files exit 0 │ +│ - every commit on the ingest tail has a valid prefix │ +│ - git blame on a sampled file walks back into upstream │ +└─────────────────────────────────────────────────────────────────┘ +``` + +After Phase A succeeds, the operator opens a draft PR (per +`pr-no-unsolicited` + `pr-always-draft`). The temp dir is destroyed +unless `--keep-tempdir` was passed. **Phase A makes no commits or +pushes to the upstream remote — full stop.** + +## Phase B — Upstream archival (clean clone, irreversible publish) + +Run **only after the Phase A PR has merged into ITK `main`.** This is +the gate: until ITK has accepted the migration, the upstream archive is +not authorized. + +### Operator command + +```bash +archive-remote-module.sh [--no-archive-flip] [--dry-run] +``` + +### What Phase B does + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Pre-flight │ +│ - The Phase A ingest PR has merged on ITK main │ +│ - git, gh CLI authenticated, network reachable │ +│ - whitelist file (same one used in Phase A) accessible │ +├─────────────────────────────────────────────────────────────────┤ +│ 2. Clean shallow clone of upstream │ +│ git clone --depth 50 git@github.com:/.git ${TMP} │ +├─────────────────────────────────────────────────────────────────┤ +│ 3. Build the removal commit on a fresh branch │ +│ - git rm -rf each whitelisted path │ +│ - leave non-whitelisted (papers, Old/, examples/, etc.) │ +│ - one commit subject: "MIGRATE: Remove files migrated to ITK │ +│ " │ +├─────────────────────────────────────────────────────────────────┤ +│ 4. Promote MIGRATION_README.md to README.md │ +│ - per feedback_ingest_archive_readme.md │ +│ - rename old README.rst → info.rst (history preserved) │ +│ - rename MIGRATION_README.md → README.md │ +│ - one additional commit: │ +│ "DOC: Promote migration notice to README.md" │ +├─────────────────────────────────────────────────────────────────┤ +│ 5. Push to upstream main │ +│ git push origin HEAD:main │ +├─────────────────────────────────────────────────────────────────┤ +│ 6. Verify GitHub-rendered README is the migration notice │ +│ gh api repos///readme --jq .content | base64 -d │ +│ | head -10 must contain "Migrated to ITK main" or equivalent │ +├─────────────────────────────────────────────────────────────────┤ +│ 7. Flip archived=true (skip if --no-archive-flip) │ +│ gh api -X PATCH repos// -F archived=true │ +├─────────────────────────────────────────────────────────────────┤ +│ 8. Cleanup │ +│ - delete the throwaway clone │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Why Phase B uses a fresh clone + +The Phase A throwaway clone has been mutated by `filter-repo` and is +**not safe to push back** to the upstream remote — its commit graph and +object pool are no longer 1:1 with the upstream. A fresh clone is the +only way to safely publish a deletion commit on top of upstream's actual +HEAD without inadvertently force-pushing rewritten history to the +upstream archive (which would destroy the original development history +that the archive is supposed to preserve). + +## Shared artifact: the whitelist + +Both phases consume the same whitelist. It lives at: + +``` +Utilities/Maintenance/RemoteModuleIngest/whitelists/.list +``` + +Format: one path/glob per line, relative to the upstream repo root, +shell-style globs. Lines starting with `#` are comments. Example for +`IOMeshSTL`: + +``` +# Public headers +include/** + +# Test sources + fixtures +test/** + +# CMake / module metadata +CMakeLists.txt +itk-module.cmake + +# Wrapping +wrapping/** + +# License (always) +LICENSE +``` + +Phase A uses this with `--paths-from-file`; Phase B inverts it (delete +everything on the whitelist; keep everything else, plus +`MIGRATION_README.md` if present). + +## Implementation choices (from 2026-05-04 design discussion) + +| Question | Decision | +|---|---| +| Where does the upstream "removal" commit live? | **Phase B publishes it to upstream `main` directly** (replaces the manual archival PR pattern from `feedback_ingest_archive_readme.md`) | +| Auto-prefix vs. report-and-fail for commit subjects? | **Heuristic auto-prefix with sidecar log** — fast, operator can spot-check the log before merge | +| Format-history scope? | **All commits** — uniform `git blame`, one-time wall-time cost is acceptable | +| Python formatter? | **`black`** on every `.py` blob across all history (in addition to clang-format on C++) | +| Where do v3 scripts go? | Kept in tree, renamed `*_v3.{sh,py}` until v4 has been used for two successful ingests, then retired to `Utilities/Maintenance/RemoteModuleIngest/retired/v3/` | + +## Operator gate between Phase A and Phase B + +``` +Phase A: ingest-module-v4.sh IOMeshSTL IO + → draft PR opened + → reviewer feedback, fixups, force-pushes (Phase A re-runs OK) + → PR marked ready for review + → MAINTAINER MERGES THE PR + ──────────────────────────────────────── + → Phase B: archive-remote-module.sh IOMeshSTL + → upstream main now shows MIGRATION_README.md as repo README + → upstream archived=true +``` + +This gate is the central correctness property of v4: **upstream is +never touched until ITK has accepted the migration.** + +## Heuristic prefix mapping + +When `sanitize-history.py` encounters a commit subject without a valid +prefix, it auto-prepends one based on the table below. Every decision +is logged with ` ` so the +operator can audit before merge. + +| Subject contains... | Prefix | +|---|---| +| `fix`, `bug`, `crash`, `segfault`, `leak`, `regression` | `BUG:` | +| `cmake`, `compil`, `warning`, `build`, ` ci ` / `ci:`, `clang-format` config | `COMP:` | +| `doc`, `readme`, `comment`, `license`, `header text` | `DOC:` | +| `style`, `whitespace`, `format`, `rename`, `lint`, `clean up` | `STYLE:` | +| `perf`, `optimi`, `speed`, `faster`, `efficien` | `PERF:` | +| anything else | `ENH:` (default) | + +These rules are intentionally **conservative** — when in doubt, default +to `ENH:`. Misclassifications get caught by the operator reading the +sidecar log; the cost of `ENH: STYLE-cleanup-of-headers` slipping past +review is much lower than the cost of `STYLE: Add new IO format +support` mis-prefixed. + +## Known artifacts at PR-review time + +### Unavoidable: ghostflow root-commit failure + +The Mode-A `--allow-unrelated-histories` merge brings the upstream +module's whole lineage into the PR diff, including its first commit +(zero parents). ITK's `ghostflow-check-main` rejects every PR that +introduces a root commit: + +``` +commit not allowed; it is a root commit. +``` + +This single error is **expected and accepted** for every v4 ingest — +maintainers override at merge time. Removing the root would force +linearization (squash or rebase-without-merges) and forfeit the +merge-topology requirement (per `feedback_ingest_merge_topology.md`), +which the project considers a worse trade. + +If `ghostflow-check-main` reports any error other than the single +root-commit line, that error **is** a real problem for the operator to +fix before maintainer review. Keep an eye on the message body. + +### Patterns Phase A now sanitizes automatically + +The following recurring upstream artifacts are stripped or rewritten +by the v4 sanitizer; operators do not need to fix them by hand: + +| Artifact | Where handled | Why | +|---|---|---| +| `**/.ExternalData_*` (e.g. `.ExternalData_MD5_` baseline cache) | `ingest-module-v4.sh` deny-pass | Local fetch-cache from upstream's CTest run; ExternalData regenerates from `.cid` / `.md5` sidecars on demand. (@dzenanz, PR #6206) | +| `if(NOT ITK_SOURCE_DIR) ... find_package(ITK) ... else() itk_module_impl() endif()` standalone-build guard in module `CMakeLists.txt` | `sanitize-history.py:patch_standalone_build_guard` | In-tree, `ITK_SOURCE_DIR` is always defined; the if-branch is dead code. (@dzenanz, PR #6206) | +| `cmake_minimum_required(VERSION X.Y.Z)` line at the top of an ingested module's `CMakeLists.txt` | `sanitize-history.py:patch_drop_cmake_minimum_required` | ITK's top-level CMakeLists pins a higher minimum; per-module declarations are redundant and frequently **lower** than the ITK floor (3.10.2 is common upstream). (@dzenanz, PR #6215 IOFDF) | +| `README.rst` references in CMake `file(READ ...)` calls | `sanitize-history.py:patch_readme_reference` | Phase B archival promotes `MIGRATION_README.md` to `README.md`; in-tree consumers read the markdown form | +| `get_filename_component(...) / file(READ README.md DOCUMENTATION)` preamble + `DESCRIPTION "${DOCUMENTATION}"` in `itk-module.cmake` | `sanitize-history.py:patch_dynamic_description` | Archival `README.md` contains semicolons and `[` characters that CMake list expansion splits into spurious `itk_module()` arguments, producing `CMake Warning (dev): Unknown argument` on every configure (observed: RLEImage, SplitComponents, IOFDF, IOMeshMZ3, IOMeshSTL; fixed in ITK PR #6220) | +| `*.orig`, `*.rej`, `*.BACKUP.*`, `*.LOCAL.*`, `*.REMOTE.*`, `*.BASE.*` | deny-pass | Leftover merge-conflict artifacts | +| Scaffolding (`Dockerfile*`, `azure-pipelines*.yml`, `.github/`, `.travis.yml`, `.circleci/`, `tox.ini`, `pyproject.toml`, `setup.py`, `.clang-format`, `.pre-commit-config.yaml`, …) | deny-pass | Module's per-repo CI/packaging is irrelevant in-tree | + +Each sanitizer prints a ` patches` line in the run summary so +the operator can confirm the rule fired (or didn't) on a given module. + +### Code-level patterns commonly flagged by Greptile post-ingest + +The patterns below still require **human judgment** to fix — they're +semantic, not mechanical, and the v4 sanitizer leaves them alone. +The list is intentionally short: only patterns observed on multiple +v3/v4 ingests are listed. Treat it as the operator's pre-ready-for- +review checklist. + +| Pattern | Where seen | Resolution | +|---|---|---| +| Signed `int32_t` / `int` used for spec-defined unsigned counts (e.g. file-format triangle counts, voxel counts) | IOMeshSTL #6206 | Switch to `uint32_t` / `SizeValueType`; retype `ByteSwapper<>`; verify decrement loops | +| Missing `override` specifier on virtual base-class method overrides | (universal) | Add `override`; if base method is non-virtual, document why the derived method shadows | +| Dead `return;` after `itkExceptionMacro(...)` | IOMeshSTL #6206 | `itkExceptionMacro` throws; the trailing `return;` is unreachable — delete | +| `inputFilename=` in error path that opens the **output** stream (and vice-versa) | IOMeshSTL #6206 | Cosmetic but misleading; correct the label | +| Test pipeline `Update()` not wrapped in `ITK_TRY_EXPECT_NO_EXCEPTION` | IOMeshSTL #6206 | Wrap so failure surfaces with a descriptive message instead of an unhandled-exception crash | +| `GetBuffer() const` returning a non-`const` smart pointer (allows mutation through a `const` receiver) | RLEImage #6208 | Return `BufferType::ConstPointer`; consider whether `m_Buffer`'s `mutable` is necessary | +| Global-namespace forward declaration + `friend class ::Foo;` inside an ITK header, where `Foo` is not part of ITK | RLEImage #6208 | Strip both the forward decl and the `friend` grant; they are remote-module-internal coupling | +| Stray positional argument after the function name in `itk_add_test(... COMMAND ...Driver fnName )` | RLEImage #6208 | Test driver passes `argv[2..n]` to `fnName` as arguments — it does not invoke a second function. Remove the stray, register the second test separately if needed | +| Allocator overrides whose `bool initialize` parameter is silently ignored | RLEImage #6208 | Verify the override has an internal invariant requiring unconditional init **before** "fixing" — for some module types (e.g. RLEImage's RLE-line buffer) the unconditional fill IS the contract; document it instead | + +When an upcoming ingest hits a new pattern that recurs across modules, +extend this table; it is the durable channel for v4 review-knowledge. + +## What this strategy explicitly does NOT change from v3 + +- The Mode-A (`--no-ff --allow-unrelated-histories`) merge into ITK +- Merge-topology preservation requirement (per + `feedback_ingest_merge_topology.md`) +- The Mode-A subject convention for the merge commit (`ENH: Ingest …`) +- The whitelist as the source of truth for what migrates +- The directory layout (`Modules///`) + +## Provenance + +- 2026-05-04 — Initial v4 design (Hans Johnson + Claude Code session) in + response to coupled-archival pain in v3 ingests (LabelErodeDilate, + GLI, MGHIO, FastBilateral, MeshNoise, Montage) +- Smoke-tested on `IOMeshSTL` before being applied to any other module diff --git a/Utilities/Maintenance/RemoteModuleIngest/archive-remote-module.sh b/Utilities/Maintenance/RemoteModuleIngest/archive-remote-module.sh new file mode 100755 index 000000000000..7055eb9de1a7 --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/archive-remote-module.sh @@ -0,0 +1,315 @@ +#!/usr/bin/env bash +# archive-remote-module.sh — Phase B driver for v4 ingestion +# +# Phase B is the IRREVERSIBLE, UPSTREAM-PUBLISH half. It runs ONLY +# AFTER the Phase A ingest PR has merged into ITK main. Phase B: +# +# 1. Clones the upstream remote module fresh (NEVER reuses Phase A's +# filter-repo'd clone — that history is rewritten and unsafe to +# push back). +# 2. Removes the whitelisted files (the ones now living in ITK). +# 3. Promotes MIGRATION_README.md to README.md so the GitHub-rendered +# landing page shows the migration notice (per +# feedback_ingest_archive_readme.md). +# 4. Pushes the resulting two commits to upstream main. +# 5. Verifies via gh api that the rendered README contains the +# migration notice. +# 6. Flips archived=true on the GitHub repo. +# +# See INGESTION_STRATEGY_v4.md for the full design. +# +# Usage: +# archive-remote-module.sh [options] +# +# Options: +# --upstream-url URL Override the GIT_REPOSITORY parsed from +# Modules/Remote/.remote.cmake. +# --whitelist FILE Override the default whitelist location +# (whitelists/.list, this directory). +# --itk-pr-number N The merged Phase A PR number, included in +# the upstream removal-commit message. +# --no-archive-flip Push the deletion + README commits, but do +# NOT flip archived=true. Use when the +# upstream still needs hand-tweaks. +# --dry-run Build the commits in a tempdir and show +# what would be pushed; do not push. +# --keep-tempdir Don't delete the throwaway clone. +# -h, --help Show this help. +# +# Exit codes: +# 0 — success +# 1 — argument or environment failure +# 2 — clone, commit, or push failure +# 3 — README verification failed (rendered README is not the migration notice) + +set -euo pipefail + +# -------------------------------------------------------------------- +# Helpers +# -------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +info() { printf '==> %s\n' "$*"; } +warn() { printf 'WARN: %s\n' "$*" >&2; } +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } + +show_help() { + sed -n '2,/^$/{ s/^# \?//; p }' "$0" + exit 0 +} + +# -------------------------------------------------------------------- +# Argument parsing +# -------------------------------------------------------------------- +MODULE="" +UPSTREAM_URL="" +WHITELIST="" +ITK_PR_NUMBER="" +NO_ARCHIVE_FLIP=false +DRY_RUN=false +KEEP_TEMPDIR=false + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) show_help ;; + --upstream-url) UPSTREAM_URL="$2"; shift 2 ;; + --whitelist) WHITELIST="$2"; shift 2 ;; + --itk-pr-number) ITK_PR_NUMBER="$2"; shift 2 ;; + --no-archive-flip) NO_ARCHIVE_FLIP=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --keep-tempdir) KEEP_TEMPDIR=true; shift ;; + -*) die "Unknown option: $1" ;; + *) + [[ -z "$MODULE" ]] && MODULE="$1" || die "Unexpected positional argument: $1" + shift + ;; + esac +done + +[[ -n "$MODULE" ]] || die "Module name required (e.g., IOMeshSTL)" + +# -------------------------------------------------------------------- +# Preflight +# -------------------------------------------------------------------- +command -v gh >/dev/null || die "gh CLI required" +command -v git >/dev/null || die "git required" + +# Resolve whitelist +if [[ -z "$WHITELIST" ]]; then + WHITELIST="$SCRIPT_DIR/whitelists/$MODULE.list" +fi +[[ -r "$WHITELIST" ]] || die "Whitelist not readable: $WHITELIST" + +# Try to infer upstream URL from the now-removed remote.cmake by looking +# back in git history of an ITK checkout, if available. +if [[ -z "$UPSTREAM_URL" ]]; then + ITK_SRC="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [[ -n "$ITK_SRC" ]]; then + REMOTE_FILE="$ITK_SRC/Modules/Remote/$MODULE.remote.cmake" + if [[ -f "$REMOTE_FILE" ]]; then + UPSTREAM_URL=$(awk '/GIT_REPOSITORY/ { print $2 }' "$REMOTE_FILE" | head -1) + else + # remote.cmake already deleted by Phase A — try the file's last + # version before deletion. + UPSTREAM_URL=$( + git -C "$ITK_SRC" log --all --pretty=format: --diff-filter=D \ + -p -- "Modules/Remote/$MODULE.remote.cmake" 2>/dev/null \ + | awk '/^-\s*GIT_REPOSITORY/ { print $2 }' \ + | head -1 + ) || true + fi + fi +fi +[[ -n "$UPSTREAM_URL" ]] \ + || die "Could not infer upstream URL; pass --upstream-url explicitly." + +# Parse owner/repo for gh-api calls +OWNER_REPO=$(echo "$UPSTREAM_URL" \ + | sed -E 's#^(git@github.com:|https://github.com/)##; s#\.git$##') +[[ "$OWNER_REPO" =~ ^[^/]+/[^/]+$ ]] \ + || die "Could not parse owner/repo from $UPSTREAM_URL (got: $OWNER_REPO)" + +info "Phase B — Upstream archival (fresh clone, irreversible publish)" +info " Module: $MODULE" +info " Upstream URL: $UPSTREAM_URL" +info " Owner/repo: $OWNER_REPO" +info " Whitelist: $WHITELIST" +[[ -n "$ITK_PR_NUMBER" ]] && info " ITK PR ref: #$ITK_PR_NUMBER" +$DRY_RUN && info " Mode: --dry-run (no push, no archive flip)" +$NO_ARCHIVE_FLIP && info " --no-archive-flip: will push but not archive" + +# -------------------------------------------------------------------- +# Operator gate: confirm the Phase A PR has merged. +# -------------------------------------------------------------------- +if [[ -n "$ITK_PR_NUMBER" ]] && ! $DRY_RUN; then + PR_STATE=$(gh pr view "$ITK_PR_NUMBER" \ + --repo InsightSoftwareConsortium/ITK \ + --json state,mergedAt --jq '"\(.state) merged=\(.mergedAt // "no")"' \ + 2>/dev/null) || PR_STATE="(could not query)" + info " ITK PR #$ITK_PR_NUMBER state: $PR_STATE" + if [[ "$PR_STATE" != *"MERGED"* ]]; then + die "ITK PR #$ITK_PR_NUMBER does not appear merged. Phase B requires +the Phase A PR to be merged on ITK main first. Aborting." + fi +fi + +# -------------------------------------------------------------------- +# Work area +# -------------------------------------------------------------------- +WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/archive-v4-$MODULE.XXXXXX") +cleanup() { + if $KEEP_TEMPDIR; then + info "Tempdir preserved at: $WORKDIR" + else + rm -rf "$WORKDIR" + fi +} +trap cleanup EXIT + +# -------------------------------------------------------------------- +# Step 1: shallow clone of upstream (fresh — never the Phase A clone) +# -------------------------------------------------------------------- +CLONE="$WORKDIR/$MODULE" +info "Cloning upstream into $CLONE (depth 50, fresh — not Phase A's clone)..." +git clone --quiet --depth 50 "$UPSTREAM_URL" "$CLONE" +DEFAULT_BRANCH=$(git -C "$CLONE" symbolic-ref --short HEAD 2>/dev/null || echo main) +info " Default branch: $DEFAULT_BRANCH" + +# -------------------------------------------------------------------- +# Step 2: build the removal commit. Walk the whitelist line-by-line, +# `git rm -rf` each path that exists. Commit with a clear subject. +# -------------------------------------------------------------------- +info "Removing whitelisted (now-migrated) files..." +REMOVED_COUNT=0 +( + cd "$CLONE" + while IFS= read -r line; do + line="${line%%#*}" # strip comment + line="${line//[$'\t\r ']/}" # strip whitespace (paths shouldn't contain spaces) + [[ -z "$line" ]] && continue + # Globs are evaluated relative to the repo root. + matches=( $(eval "ls -d $line 2>/dev/null" || true) ) + if (( ${#matches[@]} > 0 )); then + git rm -rf "${matches[@]}" >/dev/null 2>&1 || true + REMOVED_COUNT=$(( REMOVED_COUNT + ${#matches[@]} )) + fi + done < "$WHITELIST" +) + +# Commit only if anything was removed +if (( REMOVED_COUNT == 0 )); then + warn "No whitelisted paths matched in upstream; nothing to remove." + warn "Skipping removal commit — proceeding to README promotion." +else + COMMIT_MSG_RM="MIGRATE: Remove files migrated to ITK main" + if [[ -n "$ITK_PR_NUMBER" ]]; then + COMMIT_MSG_RM="$COMMIT_MSG_RM (ITK PR #$ITK_PR_NUMBER)" + fi + ( + cd "$CLONE" + git -c user.name="ITK Migration Bot" \ + -c user.email="itk-migration@itk.org" \ + commit -m "$COMMIT_MSG_RM" \ + -m "These files have been ingested into the ITK main repository. +This repository is being archived; see README for details." + ) || die "Removal commit failed" + info " Removed $REMOVED_COUNT path(s); commit: $COMMIT_MSG_RM" +fi + +# -------------------------------------------------------------------- +# Step 3: promote MIGRATION_README.md to README.md so the rendered +# landing page on GitHub shows the migration notice +# (per feedback_ingest_archive_readme.md). +# +# Cases handled: +# (a) MIGRATION_README.md exists, README.rst (or other non-.md) exists +# → mv README.rst -> info.rst ; mv MIGRATION_README.md -> README.md +# (b) MIGRATION_README.md exists, README.md exists (already .md) +# → mv MIGRATION_README.md README.md (overwrites the old README; +# keeping the original around as info.md preserves its history) +# (c) MIGRATION_README.md does not exist +# → warn — operator must add it manually before re-running. +# -------------------------------------------------------------------- +PROMOTED=false +( + cd "$CLONE" + if [[ ! -f MIGRATION_README.md ]]; then + warn "No MIGRATION_README.md in upstream root." + warn "Add one (e.g., copying the LabelErodeDilate template) and re-run." + exit 0 + fi + if [[ -f README.rst ]]; then + git mv README.rst info.rst + elif [[ -f README.md ]]; then + git mv README.md info.md + fi + git mv MIGRATION_README.md README.md + + COMMIT_MSG_DOC="DOC: Promote migration notice to README.md" + git -c user.name="ITK Migration Bot" \ + -c user.email="itk-migration@itk.org" \ + commit -m "$COMMIT_MSG_DOC" \ + -m "GitHub renders README.md (or README.rst) on the landing page, +not MIGRATION_README.md. Promoting the migration notice to README.md +ensures visitors to the archived repo see the redirect." +) +if [[ -f "$CLONE/README.md" ]] && grep -qi "migrat" "$CLONE/README.md" 2>/dev/null; then + PROMOTED=true +fi + +# -------------------------------------------------------------------- +# Step 4: dry-run stops here +# -------------------------------------------------------------------- +if $DRY_RUN; then + info "" + info "Dry-run complete. Commits prepared in $CLONE." + info "Inspect with:" + info " cd $CLONE && git log --oneline | head -5" + exit 0 +fi + +# -------------------------------------------------------------------- +# Step 5: push to upstream main (the irreversible step) +# -------------------------------------------------------------------- +info "Pushing to upstream $DEFAULT_BRANCH..." +( + cd "$CLONE" + git push origin "HEAD:$DEFAULT_BRANCH" +) || die "Push to upstream failed. Check authentication and write access." + +# -------------------------------------------------------------------- +# Step 6: verify GitHub renders the migration notice +# -------------------------------------------------------------------- +info "Verifying GitHub-rendered README is the migration notice..." +RENDERED=$(gh api "repos/$OWNER_REPO/readme" --jq '.content' 2>/dev/null \ + | base64 -d 2>/dev/null | head -20 || true) +if [[ -z "$RENDERED" ]]; then + warn "Could not fetch rendered README via gh api." + warn "Manual check needed: https://github.com/$OWNER_REPO" +elif ! echo "$RENDERED" | grep -qi "migrat\|archive\|moved to ITK"; then + warn "Rendered README does not appear to be the migration notice." + warn "First lines:" + printf '%s\n' "$RENDERED" | sed 's/^/ /' >&2 + die "README precedence verification failed (per feedback_ingest_archive_readme.md)." +else + info " OK: rendered README contains migration language." +fi + +# -------------------------------------------------------------------- +# Step 7: flip archived=true (skip if --no-archive-flip) +# -------------------------------------------------------------------- +if $NO_ARCHIVE_FLIP; then + info "Skipping archived=true flip per --no-archive-flip." + info "When ready, run:" + info " gh api -X PATCH repos/$OWNER_REPO -F archived=true" +else + info "Flipping archived=true on $OWNER_REPO..." + gh api -X PATCH "repos/$OWNER_REPO" -F archived=true >/dev/null \ + || die "Failed to flip archived=true. Check permissions on $OWNER_REPO." + info " OK: $OWNER_REPO is now archived." +fi + +info "" +info "Phase B complete." +info " Upstream removal commit pushed to $UPSTREAM_URL" +info " README precedence: rendered README is the migration notice" +$NO_ARCHIVE_FLIP || info " Repo state: archived" diff --git a/Utilities/Maintenance/RemoteModuleIngest/ingest-module-v4.sh b/Utilities/Maintenance/RemoteModuleIngest/ingest-module-v4.sh new file mode 100755 index 000000000000..056c33a007ee --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/ingest-module-v4.sh @@ -0,0 +1,426 @@ +#!/usr/bin/env bash +# ingest-module-v4.sh — Phase A driver for v4 remote-module ingestion +# +# Phase A is the LOCAL, REVERSIBLE half of the ingestion process. It +# rewrites the upstream remote-module's history into a form suitable +# for a Mode-A merge into ITK proper. Phase A makes NO commits or +# pushes to the upstream remote — the local clone is throwaway. The +# subsequent upstream-archival publish step lives in Phase B +# (archive-remote-module.sh) and is gated on the Phase A PR being +# merged into ITK main. +# +# See INGESTION_STRATEGY_v4.md for the full design. +# +# Usage: +# ingest-module-v4.sh [options] +# +# Options: +# --upstream-url URL Override the GIT_REPOSITORY parsed from +# Modules/Remote/.remote.cmake. +# --whitelist FILE Override the default whitelist location +# (whitelists/.list, this directory). +# --dry-run Run filter-repo + sanitize passes but do +# NOT merge into ITK. Reports what would +# land. +# --keep-tempdir Don't delete the rewritten clone after +# finishing (useful with --dry-run for +# post-mortem inspection). +# --skip-format Skip sanitize-history.py. ONLY for debugging +# the filter-repo passes; the resulting branch +# will not pass ITK's pre-commit gate. +# -h, --help Show this help. +# +# Exit codes: +# 0 — success +# 1 — argument or environment failure +# 2 — filter-repo, sanitize, or merge failure +# 3 — post-merge verification failed (pre-commit, prefix scan) + +set -euo pipefail + +# -------------------------------------------------------------------- +# Helpers +# -------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +info() { printf '==> %s\n' "$*"; } +warn() { printf 'WARN: %s\n' "$*" >&2; } +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } + +show_help() { + sed -n '2,/^$/{ s/^# \?//; p }' "$0" + exit 0 +} + +# -------------------------------------------------------------------- +# Argument parsing +# -------------------------------------------------------------------- +MODULE="" +DEST_GROUP="" +UPSTREAM_URL="" +WHITELIST="" +DRY_RUN=false +KEEP_TEMPDIR=false +SKIP_FORMAT=false + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) show_help ;; + --upstream-url) UPSTREAM_URL="$2"; shift 2 ;; + --whitelist) WHITELIST="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --keep-tempdir) KEEP_TEMPDIR=true; shift ;; + --skip-format) SKIP_FORMAT=true; shift ;; + -*) die "Unknown option: $1" ;; + *) + if [[ -z "$MODULE" ]]; then MODULE="$1" + elif [[ -z "$DEST_GROUP" ]]; then DEST_GROUP="$1" + else die "Unexpected positional argument: $1" + fi + shift + ;; + esac +done + +[[ -n "$MODULE" ]] || die "Module name required (e.g., IOMeshSTL)" +[[ -n "$DEST_GROUP" ]] || die "Destination group required (e.g., IO, Filtering, Segmentation)" + +# -------------------------------------------------------------------- +# Preflight +# -------------------------------------------------------------------- +command -v git-filter-repo >/dev/null \ + || die "git-filter-repo required. Install with: pixi global install git-filter-repo" + +if ! $SKIP_FORMAT; then + command -v clang-format >/dev/null \ + || die "clang-format required for sanitize step (or pass --skip-format)" + command -v black >/dev/null \ + || die "black required for sanitize step (or pass --skip-format)" + command -v gersemi >/dev/null \ + || die "gersemi required for sanitize step (or pass --skip-format)" +fi + +ITK_SRC="$(git rev-parse --show-toplevel 2>/dev/null || true)" +[[ -n "$ITK_SRC" ]] || die "Must be run from inside a git checkout of ITK" +[[ -f "$ITK_SRC/itk-module.cmake" || -f "$ITK_SRC/CMakeLists.txt" ]] \ + || die "Current git root does not look like ITK: $ITK_SRC" + +if ! $DRY_RUN && [[ -n "$(git -C "$ITK_SRC" status --porcelain)" ]]; then + die "Working tree not clean; commit or stash first (or use --dry-run)" +fi + +# Resolve whitelist +if [[ -z "$WHITELIST" ]]; then + WHITELIST="$SCRIPT_DIR/whitelists/$MODULE.list" +fi +[[ -r "$WHITELIST" ]] || die "Whitelist not readable: $WHITELIST + Create it from a sibling module's whitelist as a starting point." + +# Resolve clang-format / gersemi config files from the destination ITK tree +CLANG_FORMAT_STYLE="$ITK_SRC/.clang-format" +[[ -f "$CLANG_FORMAT_STYLE" ]] \ + || die "ITK .clang-format missing at $CLANG_FORMAT_STYLE" +GERSEMI_CONFIG="$ITK_SRC/.gersemi.config" +[[ -f "$GERSEMI_CONFIG" ]] \ + || warn "ITK .gersemi.config missing at $GERSEMI_CONFIG; gersemi will use defaults" + +# Infer upstream URL from the .remote.cmake if not provided +if [[ -z "$UPSTREAM_URL" ]]; then + REMOTE_FILE="$ITK_SRC/Modules/Remote/$MODULE.remote.cmake" + [[ -f "$REMOTE_FILE" ]] \ + || die "$REMOTE_FILE not found; pass --upstream-url explicitly" + UPSTREAM_URL=$(awk '/GIT_REPOSITORY/ { print $2 }' "$REMOTE_FILE" | head -1) + [[ -n "$UPSTREAM_URL" ]] || die "Could not parse GIT_REPOSITORY from $REMOTE_FILE" +fi + +# Destination collision check +DEST_DIR="$ITK_SRC/Modules/$DEST_GROUP/$MODULE" +if ! $DRY_RUN && [[ -e "$DEST_DIR" ]]; then + die "Destination $DEST_DIR already exists; module already ingested?" +fi + +info "Phase A — Ingestion (LOCAL, throwaway)" +info " Module: $MODULE" +info " DestGroup: $DEST_GROUP" +info " Upstream URL: $UPSTREAM_URL" +info " Whitelist: $WHITELIST" +info " ITK source: $ITK_SRC" +info " Style file: $CLANG_FORMAT_STYLE" +[[ -f "$GERSEMI_CONFIG" ]] && info " Gersemi cfg: $GERSEMI_CONFIG" +$DRY_RUN && info " Mode: --dry-run" +$KEEP_TEMPDIR && info " Tempdir: will be preserved" +$SKIP_FORMAT && warn " --skip-format: result will NOT pass pre-commit" + +# -------------------------------------------------------------------- +# Work area +# -------------------------------------------------------------------- +WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/ingest-v4-$MODULE.XXXXXX") +LOG_DIR="$WORKDIR/logs" +mkdir -p "$LOG_DIR" + +cleanup() { + if $KEEP_TEMPDIR; then + info "Tempdir preserved at: $WORKDIR" + else + rm -rf "$WORKDIR" + fi +} +trap cleanup EXIT + +# -------------------------------------------------------------------- +# Step 1: mirror-clone upstream into a NON-bare clone (filter-repo +# operates on either, but we want a working tree available for +# sanitize-history.py's helper paths). +# -------------------------------------------------------------------- +CLONE="$WORKDIR/$MODULE" +info "Cloning upstream into $CLONE ..." +git clone --quiet --no-local "$UPSTREAM_URL" "$CLONE" + +UPSTREAM_SHA=$(git -C "$CLONE" rev-parse HEAD) +UPSTREAM_DEFAULT_BRANCH=$(git -C "$CLONE" symbolic-ref --short HEAD 2>/dev/null || echo main) +UPSTREAM_COMMIT_COUNT=$(git -C "$CLONE" rev-list --count HEAD) +UPSTREAM_MERGE_COUNT=$(git -C "$CLONE" rev-list --count --merges HEAD) +info " Upstream tip: $UPSTREAM_SHA" +info " Upstream default branch: $UPSTREAM_DEFAULT_BRANCH" +info " Total commits: $UPSTREAM_COMMIT_COUNT" +info " Merge commits: $UPSTREAM_MERGE_COUNT" + +# -------------------------------------------------------------------- +# Step 2: filter-repo whitelist pass — read paths from the whitelist +# file. --paths-from-file accepts shell globs, one per line, # comments. +# -------------------------------------------------------------------- +info "Running filter-repo whitelist pass (--paths-from-file)..." +( + cd "$CLONE" + git filter-repo --force \ + --paths-from-file "$WHITELIST" \ + --prune-empty always +) || die "filter-repo whitelist pass failed" + +# -------------------------------------------------------------------- +# Step 2b: scaffolding deny-pass. The whitelist admits whole +# directories (test/, wrapping/, ...) but upstream remote modules +# sometimes place CI / packaging scaffolding inside those (e.g. +# test/Docker/, wrapping/azure-pipelines.yml, .github/ inside test/). +# Strip those out across all history with a per-commit invert-glob +# pass. Without this, scaffolding leaks into ITK's history; bug +# discovered on the first Cuberille v4 ingest (test/Docker/* leaked). +# -------------------------------------------------------------------- +info "Running scaffolding deny-pattern strip pass..." +( + cd "$CLONE" + git filter-repo --force \ + --invert-paths \ + --path-glob '**/CTestConfig.cmake' \ + --path-glob '**/azure-pipelines*.yml' \ + --path-glob '**/azure-pipelines/*' \ + --path-glob '**/Dockerfile' \ + --path-glob '**/Dockerfile.*' \ + --path-glob '**/Dockerfile-*' \ + --path-glob '**/.dockerignore' \ + --path-glob '**/[Dd]ocker/*' \ + --path-glob '**/.[Dd]ocker/*' \ + --path-glob '**/Jenkinsfile' \ + --path-glob '**/.circleci/*' \ + --path-glob '**/circle.yml' \ + --path-glob '**/.travis.yml' \ + --path-glob '**/appveyor.yml' \ + --path-glob '**/.appveyor.yml' \ + --path-glob '**/.cirun.yml' \ + --path-glob '**/.gitlab-ci.yml' \ + --path-glob '**/.github/*' \ + --path-glob '**/codecov.yml' \ + --path-glob '**/.codecov.yml' \ + --path-glob '**/tox.ini' \ + --path-glob '**/pyproject.toml' \ + --path-glob '**/setup.py' \ + --path-glob '**/setup.cfg' \ + --path-glob '**/MANIFEST.in' \ + --path-glob '**/requirements*.txt' \ + --path-glob '**/environment*.yml' \ + --path-glob '**/.clang-format' \ + --path-glob '**/.clang-tidy' \ + --path-glob '**/.pre-commit-config.yaml' \ + --path-glob '**/*.orig' \ + --path-glob '**/*.rej' \ + --path-glob '**/*.BACKUP.*' \ + --path-glob '**/*.LOCAL.*' \ + --path-glob '**/*.REMOTE.*' \ + --path-glob '**/*.BASE.*' \ + --path-glob '**/.ExternalData_*' \ + --prune-empty always +) || die "filter-repo deny-pass failed" + +# -------------------------------------------------------------------- +# Step 3: subdirectory move into Modules/// +# -------------------------------------------------------------------- +info "Moving into Modules/$DEST_GROUP/$MODULE/ ..." +( + cd "$CLONE" + git filter-repo --force \ + --to-subdirectory-filter "Modules/$DEST_GROUP/$MODULE" \ + --prune-empty always +) || die "filter-repo subdirectory pass failed" + +POST_FILTER_COMMITS=$(git -C "$CLONE" rev-list --count HEAD) +POST_FILTER_MERGES=$(git -C "$CLONE" rev-list --count --merges HEAD) +info " After filter-repo: $POST_FILTER_COMMITS commits, $POST_FILTER_MERGES merges" + +# -------------------------------------------------------------------- +# Step 4: sanitize-history.py — clang-format every C++ blob, black +# every .py blob, prefix every commit subject with a valid ITK prefix. +# -------------------------------------------------------------------- +if $SKIP_FORMAT; then + warn "Skipping sanitize-history.py (per --skip-format)" +else + info "Running sanitize-history.py (clang-format + black + gersemi + ws/eof + prefix)..." + GERSEMI_ARG=() + [[ -f "$GERSEMI_CONFIG" ]] && GERSEMI_ARG=(--gersemi-config "$GERSEMI_CONFIG") + python3 "$SCRIPT_DIR/sanitize-history.py" \ + --repo "$CLONE" \ + --clang-format-style "$CLANG_FORMAT_STYLE" \ + "${GERSEMI_ARG[@]}" \ + --log-dir "$LOG_DIR" \ + || die "sanitize-history.py failed" + info " Sanitize logs in: $LOG_DIR/" +fi + +# -------------------------------------------------------------------- +# Step 5: topology verification. If upstream had merges, the rewritten +# branch MUST still have merges (filter-repo preserves merge topology). +# This guards against regressions where a future change to filter-repo +# args silently linearizes — see feedback_ingest_merge_topology.md. +# -------------------------------------------------------------------- +POST_SANITIZE_COMMITS=$(git -C "$CLONE" rev-list --count HEAD) +POST_SANITIZE_MERGES=$(git -C "$CLONE" rev-list --count --merges HEAD) +info "" +info "Final rewritten history:" +info " Commits: $UPSTREAM_COMMIT_COUNT -> $POST_SANITIZE_COMMITS" +info " Merges: $UPSTREAM_MERGE_COUNT -> $POST_SANITIZE_MERGES" + +if (( UPSTREAM_MERGE_COUNT > 0 )) && (( POST_SANITIZE_MERGES == 0 )); then + die "Topology violation: upstream had $UPSTREAM_MERGE_COUNT merge(s) but rewritten history has 0. + This indicates linearization, which is forbidden per feedback_ingest_merge_topology.md." +fi + +# -------------------------------------------------------------------- +# Step 6: prefix-validation scan. Every commit subject on the rewritten +# branch must start with a valid ITK prefix after sanitize-history.py +# ran. Catch any escapes. +# -------------------------------------------------------------------- +if ! $SKIP_FORMAT; then + info "Verifying every commit subject has a valid ITK prefix..." + BAD_SUBJECTS=$( + git -C "$CLONE" log --format='%H %s' \ + | awk '$2 !~ /^(BUG|COMP|DOC|ENH|PERF|STYLE|WIP):/ { print }' + ) + if [[ -n "$BAD_SUBJECTS" ]]; then + warn "" + warn "=================================================================" + warn " PREFIX VIOLATION: commits without a valid prefix found. " + warn " sanitize-history.py should have caught these — investigate. " + warn "" + printf '%s\n' "$BAD_SUBJECTS" >&2 | head -10 + warn "=================================================================" + die "Refusing to merge into ITK with non-conforming subjects." + fi + info " OK: every commit has a valid prefix." +fi + +# -------------------------------------------------------------------- +# Step 7: dry-run stops here +# -------------------------------------------------------------------- +if $DRY_RUN; then + info "" + info "Dry-run complete. No changes made to ITK working tree." + info "Rewritten clone is at: $CLONE" + info "Sanitize logs at: $LOG_DIR/" + info "" + info "To inspect:" + info " cd $CLONE" + info " git log --oneline --graph | head -50" + info " cat $LOG_DIR/commit-prefix-log.txt | head -30" + exit 0 +fi + +# -------------------------------------------------------------------- +# Step 8: collect authors for Co-authored-by trailers in merge message +# -------------------------------------------------------------------- +info "Collecting author list for Co-authored-by trailers..." +PRIMARY_AUTHOR=$( + git -C "$CLONE" log --format='%an <%ae>' HEAD \ + | sort | uniq -c | sort -rn | head -1 | sed 's/^ *[0-9]* *//' +) +readarray -t ALL_AUTHORS < <(git -C "$CLONE" log --format='%an <%ae>' HEAD | sort -u) +CO_AUTHOR_LINES="" +for a in "${ALL_AUTHORS[@]}"; do + [[ "$a" != "$PRIMARY_AUTHOR" ]] && CO_AUTHOR_LINES+="Co-authored-by: $a"$'\n' +done + +# -------------------------------------------------------------------- +# Step 9: Mode-A merge into the current ITK branch +# -------------------------------------------------------------------- +MERGE_MSG=$(cat < $POST_SANITIZE_MERGES merge(s). + +Primary author: $PRIMARY_AUTHOR + +$CO_AUTHOR_LINES +EOF +) + +info "Merging filter-repo+sanitize output into $(git -C "$ITK_SRC" rev-parse --short HEAD)..." +( + cd "$ITK_SRC" + git remote add ingest-v4-tmp "$CLONE" 2>/dev/null || true + git fetch --quiet ingest-v4-tmp \ + "$UPSTREAM_DEFAULT_BRANCH:ingest-v4-tmp-default" + git merge --allow-unrelated-histories --no-ff \ + -m "$MERGE_MSG" \ + ingest-v4-tmp-default + git remote remove ingest-v4-tmp + git branch -D ingest-v4-tmp-default +) || die "Merge failed" + +# -------------------------------------------------------------------- +# Step 10: post-merge verification +# -------------------------------------------------------------------- +info "" +info "Post-merge verification..." +if ! pre-commit run --all-files >/dev/null 2>&1; then + warn "pre-commit reported issues — re-run interactively to inspect:" + warn " pre-commit run --all-files" + exit 3 +fi +info " OK: pre-commit run --all-files exits 0." + +info "" +info "Phase A complete. Required follow-up commits (in this PR):" +info " 1. DOC: Add Modules/$DEST_GROUP/$MODULE/README.md pointing at archived upstream" +info " 2. COMP: Remove Modules/Remote/$MODULE.remote.cmake" +info " 3. ENH: Add -DModule_$MODULE:BOOL=ON to pyproject.toml configure-ci" +info " 4. Verify local build + tests pass:" +info " pixi run -e cxx configure-ci && pixi run -e cxx build" +info " ctest --test-dir build -R $MODULE" +info "" +info "After this PR has merged into ITK main, run Phase B:" +info " $SCRIPT_DIR/archive-remote-module.sh $MODULE" +info " (Phase B publishes the upstream removal commit + flips archived=true.)" diff --git a/Utilities/Maintenance/RemoteModuleIngest/sanitize-history.py b/Utilities/Maintenance/RemoteModuleIngest/sanitize-history.py new file mode 100755 index 000000000000..fd91d62efebb --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/sanitize-history.py @@ -0,0 +1,810 @@ +#!/usr/bin/env python3 +"""sanitize-history.py — Apply ITK formatting and commit-prefix conventions +to every commit on a filter-repo'd ingestion branch. + +Per the v4 strategy (Phase A, step 5), this script walks the rewritten +ingestion branch and applies the SAME formatters that ITK's +`.pre-commit-config.yaml` enforces, so the rewritten history is gated +through identical rules. Concretely, for every blob: + + Content-aware (filename-blind, sniff-driven): + 1. C/C++ blobs -> `clang-format -style=file` (ITK's .clang-format) + 2. Python blobs -> `black` + 3. CMake blobs -> `gersemi --config .gersemi.config` + 4. All text blobs -> trailing-whitespace strip + + end-of-file fixer (final \\n) + + mixed-line-ending (CRLF -> LF) + 5. Binary / .sha* / .vtk / .vtp / .svg content -> skip (left as-is) + + Commit-message: + Examine the subject; if it does not start with a recognized ITK + prefix (BUG/COMP/DOC/ENH/PERF/STYLE), auto-prepend one based on + a conservative heuristic. Every decision is logged to a sidecar + file for operator audit. + +git-filter-repo's streaming model emits blobs before commits and does +not expose a public "fetch blob by id" API in commit_callback context, +so all blob-content rewriting happens in blob_callback. This is +filename-blind by necessity — ITK's pre-commit excludes (which are +filename-pattern based) are approximated via content-sniffing. For +typical ITK module ingests, the upstream whitelist (via filter-repo's +--paths-from-file step) has already pruned ThirdParty / Data / +build / pixi-cache paths, so the exclude set narrows to a few +content-detectable extensions (.sha / .sha512 / .svg / .vtk / .vtp). + +Usage (invoked from ingest-module-v4.sh; not normally run directly): + + python3 sanitize-history.py \\ + --repo /path/to/filter-repo'd/clone \\ + --clang-format-style /path/to/itk/.clang-format \\ + --gersemi-config /path/to/itk/.gersemi.config \\ + --log-dir /path/to/log/dir +""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +try: + import git_filter_repo as fr +except ImportError: + print( + "Error: git_filter_repo Python module not found.\n" + " Install with: pip install git-filter-repo\n" + " Or: pixi global install git-filter-repo", + file=sys.stderr, + ) + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Commit-subject prefix logic +# --------------------------------------------------------------------------- + +PREFIX_RE = re.compile(rb"^\s*(BUG|COMP|DOC|ENH|PERF|STYLE|WIP)\s*:") +SUBJECT_MAX_LEN = 78 # ITK ghostflow / kw-commit-msg rule +SUBJECT_WORDBREAK_MIN = 40 # only break at a space past this offset + + +def truncate_subject_to_body(subject: bytes, rest: bytes) -> tuple[bytes, bytes, bool]: + """If the subject exceeds SUBJECT_MAX_LEN, truncate it (preferring a word + break) and prepend the excess to the body. Returns (new_subject, new_rest, + changed).""" + if len(subject) <= SUBJECT_MAX_LEN: + return subject, rest, False + cut = SUBJECT_MAX_LEN + space_pos = subject.rfind(b" ", 0, SUBJECT_MAX_LEN) + if space_pos > SUBJECT_WORDBREAK_MIN: + cut = space_pos + head = subject[:cut].rstrip() + tail = subject[cut:].lstrip() + if not tail: + return head, rest, True + if rest: + # rest starts with at least one '\n' from the original separator + new_rest = b"\n\n" + tail + rest + else: + new_rest = b"\n\n" + tail + b"\n" + return head, new_rest, True + + +def ensure_blank_line_after_subject(rest: bytes) -> tuple[bytes, bool]: + """Subject must be followed by a blank line if a body exists. rest, when + non-empty, starts with '\\n' (the original line separator). If the next + char isn't also '\\n', insert one to make it a proper blank line.""" + if not rest: + return rest, False + if rest.startswith(b"\n\n"): + return rest, False + # rest starts with single '\n' followed by body content; convert to "\n\n" + return b"\n" + rest, True + + +def heuristic_prefix(subject: bytes) -> bytes: + s = subject.lower() + if ( + b"fix " in s + or s.startswith(b"fix") + or b" bug" in s + or b"bug " in s + or b"crash" in s + or b"segfault" in s + or b"leak" in s + or b"regression" in s + or b"corrupt" in s + or b"deadlock" in s + ): + return b"BUG" + if ( + b"cmake" in s + or b"compil" in s + or b"warning" in s + or b"build" in s + or b" ci " in s + or s.startswith(b"ci:") + or b" gcc" in s + or b"clang " in s + or b" msvc" in s + ): + return b"COMP" + if ( + b"doc " in s + or b"docs" in s + or s.startswith(b"doc") + or b"readme" in s + or b"comment" in s + or b"license" in s + or b"sphinx" in s + or b"doxygen" in s + ): + return b"DOC" + if ( + b"style" in s + or b"whitespace" in s + or b"format" in s + or b"rename" in s + or b" lint" in s + or s.startswith(b"lint") + or b"clang-format" in s + or b"cleanup" in s + or b"clean up" in s + or b"reorder" in s + ): + return b"STYLE" + if ( + b"perf" in s + or b"optim" in s + or b"speed" in s + or b"faster" in s + or b"efficien" in s + ): + return b"PERF" + return b"ENH" + + +# --------------------------------------------------------------------------- +# Content-type sniffing — filename-blind, conservative +# --------------------------------------------------------------------------- + +CXX_HINTS = ( + b"#include", + b"#pragma", + b"#ifndef ITK", + b"#ifndef itk", + b"#ifndef __itk", + b"namespace itk", + b"namespace ITK", + b"template <", + b"template<", +) + +PY_SHEBANG_HINTS = ( + b"#!/usr/bin/env python", + b"#!/usr/bin/python", + b"#!python", +) +PY_KEYWORD_HINTS = ( + b"\nimport ", + b"\nfrom ", + b"\ndef ", + b"\nclass ", +) + +CMAKE_HINTS = ( + b"cmake_minimum_required", + b"\nproject(", + b"\nadd_library(", + b"\nadd_executable(", + b"\nadd_subdirectory(", + b"\nfind_package(", + b"\ntarget_link_libraries(", + b"\ntarget_include_directories(", + b"\nset(CMAKE_", + b"\ninclude(CMakeParseArguments", + b"\nitk_module(", + b"\nitk_module_impl", + b"\nitk_module_test", + b"\nitk_add_test", + b"\nset_tests_properties", + b"\nitk_wrap_", +) + +# Single-line content-link / hash file signatures +HEX_HASH_RE = re.compile(rb"^[0-9a-f]{32,128}\s*$") + +# Skip-entirely sniffs (content cannot be safely text-fixed) +SKIP_HINTS = ( + b" bool: + """True if the leading bytes contain a NUL — fast text/binary heuristic.""" + return b"\x00" in head[:8192] + + +def is_skip_content(data: bytes, head: bytes) -> bool: + """Return True for content that should be left untouched (CID/sha + content-links, VTK volumes, SVG, etc.).""" + if any(s in head[:512] for s in SKIP_HINTS): + return True + # Single-token hex hash file (CID content-link sidecar) + if data.count(b"\n") <= 1 and HEX_HASH_RE.match(data.strip()): + return True + return False + + +def sniff_kind(data: bytes) -> str | None: + """Return 'cxx', 'py', 'cmake', 'text', or None. + + 'text' = generic plain text — will get only universal whitespace/EOF + fixers, no kind-specific formatter. + None = binary or skip-content (do nothing).""" + if not data: + return None + + head = data[:8192] + if is_binary(head): + return None + if is_skip_content(data, head): + return None + + head_lstripped = head.lstrip() + + if any(head_lstripped.startswith(s) for s in PY_SHEBANG_HINTS): + return "py" + + cxx_hit = any(hint in head for hint in CXX_HINTS) + cmake_hit = any(hint in b"\n" + head for hint in CMAKE_HINTS) + py_hit = any(hint in b"\n" + head for hint in PY_KEYWORD_HINTS) + + # Disambiguate when multiple hits: cxx > cmake > py > generic text + # CXX wins because its hints are highly specific; #include is unambiguous. + # CMake wins over python because cmake_minimum_required + add_library are + # specific, while `import` / `from` could be a comment in CMake. + if cxx_hit: + return "cxx" + if cmake_hit: + return "cmake" + if py_hit: + return "py" + return "text" + + +# --------------------------------------------------------------------------- +# Universal text fixers (correspond to pre-commit-hooks +# trailing-whitespace, end-of-file-fixer, mixed-line-ending) +# --------------------------------------------------------------------------- + + +def fix_mixed_line_ending(data: bytes) -> bytes: + # ITK convention: LF only. Convert CRLF and bare CR -> LF. + return data.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + + +def fix_trailing_whitespace(data: bytes) -> bytes: + # Strip horizontal whitespace from end of every line. + lines = data.split(b"\n") + return b"\n".join(line.rstrip(b" \t") for line in lines) + + +def fix_end_of_file(data: bytes) -> bytes: + if not data: + return data + return data.rstrip(b"\n") + b"\n" + + +def apply_universal_text_fixers(data: bytes) -> bytes: + return fix_end_of_file(fix_trailing_whitespace(fix_mixed_line_ending(data))) + + +# --------------------------------------------------------------------------- +# Formatter wrappers — fail-soft (return original on error) +# --------------------------------------------------------------------------- + + +def fmt_cxx(data: bytes, clang_format_bin: str) -> bytes: + try: + proc = subprocess.run( + [ + clang_format_bin, + "-style=file", + "-assume-filename=ingested.cxx", + ], + input=data, + capture_output=True, + check=True, + timeout=60, + ) + return proc.stdout or data + except ( + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + FileNotFoundError, + ): + return data + + +def fmt_py(data: bytes, black_bin: str) -> bytes: + try: + proc = subprocess.run( + [black_bin, "-q", "-"], + input=data, + capture_output=True, + check=True, + timeout=60, + ) + return proc.stdout or data + except ( + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + FileNotFoundError, + ): + return data + + +TEXT_FILE_EXTS = ( + b".c", + b".cc", + b".cpp", + b".cxx", + b".h", + b".hpp", + b".hxx", + b".txx", + b".tcc", + b".py", + b".cmake", + b".wrap", + b".md", + b".rst", + b".txt", + b".yml", + b".yaml", + b".json", + b".toml", + b".ini", + b".cfg", + b".in", + b".tex", + b".bib", + b".css", + b".html", + b".xml", + b".dox", +) +TEXT_FILE_BASENAMES = ( + b"cmakelists.txt", + b"license", + b"readme", + b"copying", + b"authors", + b"changelog", + b"itk-module.cmake", +) + + +def filename_is_text(filename: bytes) -> bool: + """Return True for filenames that are clearly source/text and should + therefore never carry the executable bit (used to fix mode-bit drift in + upstream module commits).""" + lower = filename.lower() + if any(lower.endswith(e) for e in TEXT_FILE_EXTS): + return True + base = lower.rsplit(b"/", 1)[-1] + return base in TEXT_FILE_BASENAMES + + +# Many ITK remote-module itk-module.cmake files do +# file(READ "${MY_CURRENT_DIR}/README.rst" DOCUMENTATION) +# to populate the CMake `DESCRIPTION` variable. The v4 whitelist +# excludes the upstream README (the operator ships a new README.md as +# part of the ingest PR), so leaving that line as-is breaks every +# historical commit's build. Rewriting the reference to README.md +# makes every commit independently buildable in the ingested tree. +README_RST_REF_RE = re.compile( + rb"(file\s*\(\s*READ\s+[^)]*?)README\.rst", re.IGNORECASE +) + + +def patch_readme_reference(data: bytes) -> tuple[bytes, bool]: + """Rewrite `file(READ ".../README.rst" ...)` -> README.md. Returns + (new_data, changed).""" + new_data, n = README_RST_REF_RE.subn(rb"\1README.md", data) + return new_data, n > 0 + + +# After ingest, the in-tree README.md is the archival migration notice. +# It contains semicolons and bracket characters that CMake's +# foreach(arg ${ARGN}) expands into spurious arguments, triggering +# CMake Warning (dev): Unknown argument [...] +# in ITKModuleMacros.cmake for every configure. +# +# The pattern to eliminate: +# get_filename_component(MY_CURRENT_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +# file(READ "${MY_CURRENT_DIR}/README.md" DOCUMENTATION) +# ... +# DESCRIPTION "${DOCUMENTATION}" +# +# Replace the two preamble lines with nothing and the DESCRIPTION arg +# with a static one-liner. +_README_FILE_READ_RE = re.compile( + rb"[ \t]*(?:get_filename_component\s*\([^)]*\)\s*\n)?" + rb"[ \t]*file\s*\(\s*READ\s+[^\n]*README\.md[^\n]*\)\s*\n", + re.IGNORECASE, +) +_DOCUMENTATION_DESCRIPTION_RE = re.compile( + rb'([ \t]*DESCRIPTION\s+)"?\$\{DOCUMENTATION\}"?', + re.IGNORECASE, +) + + +def patch_dynamic_description(data: bytes) -> tuple[bytes, bool]: + """Remove the file(READ README.md DOCUMENTATION) preamble and replace + DESCRIPTION "${DOCUMENTATION}" with a static one-liner. + + The archival README.md contains semicolons and bracket characters that + CMake list expansion splits into spurious itk_module() arguments, + producing CMake (dev) configure warnings. Returns (new_data, changed). + """ + new_data, n1 = _README_FILE_READ_RE.subn(b"", data) + new_data, n2 = _DOCUMENTATION_DESCRIPTION_RE.subn( + rb'\1"Module ingested from upstream."', new_data + ) + return new_data, (n1 + n2) > 0 + + +# Upstream remote modules wrap their top-level CMakeLists.txt in a +# standalone-build guard so the same file works both as a +# remote-fetched module and as a stand-alone CMake project: +# +# if(NOT ITK_SOURCE_DIR) +# find_package(ITK REQUIRED) +# list(APPEND CMAKE_MODULE_PATH ${ITK_CMAKE_DIR}) +# include(ITKModuleExternal) +# else() +# itk_module_impl() +# endif() +# +# In an in-tree ingested module, ITK_SOURCE_DIR is always defined, so +# the if-branch is dead code. Reviewers flag it on every ingest (e.g. +# @dzenanz on PR #6206 IOMeshSTL). Strip the wrapper here so it is +# never present in the ingest history. +STANDALONE_BUILD_GUARD_RE = re.compile( + rb"if\s*\(\s*NOT\s+ITK_SOURCE_DIR\s*\)[ \t]*\n" + rb"[ \t]*find_package\s*\(\s*ITK\s+REQUIRED\s*\)[ \t]*\n" + rb"[ \t]*list\s*\(\s*APPEND\s+CMAKE_MODULE_PATH\s+" + rb"\$\{\s*ITK_CMAKE_DIR\s*\}\s*\)[ \t]*\n" + rb"[ \t]*include\s*\(\s*ITKModuleExternal\s*\)[ \t]*\n" + rb"[ \t]*else\s*\(\s*\)[ \t]*\n" + rb"([ \t]*)itk_module_impl\s*\(\s*\)[ \t]*\n" + rb"[ \t]*endif\s*\(\s*\)", + re.IGNORECASE, +) + + +def patch_standalone_build_guard(data: bytes) -> tuple[bytes, bool]: + """Replace the `if(NOT ITK_SOURCE_DIR) ... else() itk_module_impl() endif()` + standalone-build wrapper with a bare `itk_module_impl()`. Returns + (new_data, changed). The replacement preserves the indentation of the + inner `itk_module_impl()` line so post-rewrite gersemi has a stable + starting point.""" + new_data, n = STANDALONE_BUILD_GUARD_RE.subn(rb"\1itk_module_impl()", data) + return new_data, n > 0 + + +# Upstream module CMakeLists.txt frequently begin with their own +# cmake_minimum_required(VERSION X.Y.Z) declaration (often +# 3.10.2 or earlier). In-tree, ITK's top-level CMakeLists pins a +# higher minimum, so the per-module line is at best redundant and at +# worst conflicts with policy. Reviewers (e.g. @dzenanz on PR #6215 +# IOFDF) flag it on every ingest with the comment "Version behind, +# not needed." Strip the standalone declaration here so it is never +# present in the ingest history. +CMAKE_MIN_REQUIRED_RE = re.compile( + rb"^[ \t]*cmake_minimum_required\s*\([^)]*\)[ \t]*\n", + re.IGNORECASE | re.MULTILINE, +) + + +def patch_drop_cmake_minimum_required(data: bytes) -> tuple[bytes, bool]: + """Remove any ``cmake_minimum_required(...)`` line from a CMake blob. + Returns (new_data, changed). Safe to apply to every CMake blob: + only top-level module CMakeLists files normally carry this idiom, + and ITK does not want the per-module declaration anywhere in the + ingested tree.""" + new_data, n = CMAKE_MIN_REQUIRED_RE.subn(b"", data) + return new_data, n > 0 + + +def fmt_cmake(data: bytes, gersemi_bin: str, gersemi_config: str | None) -> bytes: + cmd = [gersemi_bin] + if gersemi_config: + cmd += ["--config", gersemi_config] + cmd += ["-"] # stdin -> stdout + try: + proc = subprocess.run( + cmd, + input=data, + capture_output=True, + check=True, + timeout=60, + ) + return proc.stdout or data + except ( + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + FileNotFoundError, + ): + return data + + +# --------------------------------------------------------------------------- +# Stateful sanitizer with logging +# --------------------------------------------------------------------------- + + +class HistorySanitizer: + def __init__( + self, + *, + clang_format_bin: str, + black_bin: str, + gersemi_bin: str, + gersemi_config: str | None, + log_dir: Path, + ): + self.clang_format_bin = clang_format_bin + self.black_bin = black_bin + self.gersemi_bin = gersemi_bin + self.gersemi_config = gersemi_config + self.log_dir = log_dir + self.log_dir.mkdir(parents=True, exist_ok=True) + self.prefix_log = (log_dir / "commit-prefix-log.txt").open("w", buffering=1) + self.format_log = (log_dir / "format-actions.log").open("w", buffering=1) + self.commit_count = 0 + self.prefix_changes = 0 + self.subject_truncations = 0 + self.subject_blank_inserts = 0 + self.mode_normalizations = 0 + self.readme_ref_patches = 0 + self.dynamic_description_patches = 0 + self.standalone_guard_patches = 0 + self.cmake_min_required_drops = 0 + self.blob_count = 0 + self.format_changes = 0 + self.kind_counts: dict[str, int] = {} + + # -- callbacks ----------------------------------------------------------- + + def blob_callback(self, blob: fr.Blob, _metadata: dict) -> None: + self.blob_count += 1 + kind = sniff_kind(blob.data) + if kind is None: + return + self.kind_counts[kind] = self.kind_counts.get(kind, 0) + 1 + + original_data = blob.data + original_len = len(original_data) + + # Step 1: kind-specific formatter + if kind == "cxx": + new_data = fmt_cxx(original_data, self.clang_format_bin) + elif kind == "py": + new_data = fmt_py(original_data, self.black_bin) + elif kind == "cmake": + new_data, guard_unwrapped = patch_standalone_build_guard(original_data) + if guard_unwrapped: + self.standalone_guard_patches += 1 + new_data, cmake_min_dropped = patch_drop_cmake_minimum_required(new_data) + if cmake_min_dropped: + self.cmake_min_required_drops += 1 + new_data = fmt_cmake(new_data, self.gersemi_bin, self.gersemi_config) + new_data, readme_patched = patch_readme_reference(new_data) + if readme_patched: + self.readme_ref_patches += 1 + new_data, desc_patched = patch_dynamic_description(new_data) + if desc_patched: + self.dynamic_description_patches += 1 + else: # 'text' + new_data = original_data + + # Step 2: universal text fixers — applied to every text blob, + # even ones the kind-specific formatter already passed over. + new_data = apply_universal_text_fixers(new_data) + + if new_data == original_data: + return + + self.format_changes += 1 + try: + self.format_log.write( + f"{kind:5s} {original_len:8d} -> {len(new_data):8d} " + f"{(blob.original_id or b'').decode('utf-8', 'replace')[:12]}\n" + ) + except Exception: + pass + blob.data = new_data + + def commit_callback(self, commit: fr.Commit, _metadata: dict) -> None: + self.commit_count += 1 + + # ---- subject sanitization ---- + msg = commit.message + nl = msg.find(b"\n") + subject = msg if nl == -1 else msg[:nl] + rest = b"" if nl == -1 else msg[nl:] + + # Step 1: ensure ITK prefix + prepended = False + if not PREFIX_RE.match(subject): + prefix = heuristic_prefix(subject) + subject = prefix + b": " + subject + self.prefix_changes += 1 + prepended = True + try: + old = ( + (msg if nl == -1 else msg[:nl]) + .decode("utf-8", "replace") + .strip()[:120] + ) + sha = (commit.original_id or b"").decode("utf-8", "replace") + self.prefix_log.write(f"{sha[:12]} {prefix.decode()}: {old}\n") + except Exception: + pass + + # Step 2: enforce 78-char subject limit (ghostflow / kw-commit-msg) + subject, rest, truncated = truncate_subject_to_body(subject, rest) + if truncated: + self.subject_truncations += 1 + + # Step 3: ensure subject is followed by a blank line if a body exists + rest, blank_inserted = ensure_blank_line_after_subject(rest) + if blank_inserted: + self.subject_blank_inserts += 1 + + if prepended or truncated or blank_inserted: + commit.message = subject + rest + + # ---- file-mode normalization (clear executable bit on text files) ---- + if commit.file_changes: + for change in commit.file_changes: + if change.type not in (b"M", b"A"): + continue + if not change.filename or not change.mode: + continue + if change.mode != b"100755": + continue + if filename_is_text(change.filename): + change.mode = b"100644" + self.mode_normalizations += 1 + + def close(self) -> None: + for f in (self.prefix_log, self.format_log): + try: + f.close() + except Exception: + pass + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--repo", type=Path, required=True) + p.add_argument("--clang-format-style", type=Path, required=True) + p.add_argument( + "--gersemi-config", + type=Path, + default=None, + help="Path to ITK's .gersemi.config (optional but strongly recommended)", + ) + p.add_argument("--log-dir", type=Path, required=True) + p.add_argument( + "--clang-format-bin", default=os.environ.get("CLANG_FORMAT_BIN", "clang-format") + ) + p.add_argument("--black-bin", default=os.environ.get("BLACK_BIN", "black")) + p.add_argument("--gersemi-bin", default=os.environ.get("GERSEMI_BIN", "gersemi")) + args = p.parse_args() + + if not args.repo.exists(): + print(f"Error: repo path does not exist: {args.repo}", file=sys.stderr) + return 2 + if not args.clang_format_style.exists(): + print( + f"Error: clang-format file does not exist: {args.clang_format_style}", + file=sys.stderr, + ) + return 2 + for binname, binval in ( + ("clang-format", args.clang_format_bin), + ("black", args.black_bin), + ("gersemi", args.gersemi_bin), + ): + if shutil.which(binval) is None: + print(f"Error: {binname} not found at {binval}", file=sys.stderr) + return 2 + + target_style = args.repo / ".clang-format" + if not target_style.exists(): + target_style.write_bytes(args.clang_format_style.read_bytes()) + + gersemi_config_path: str | None = None + if args.gersemi_config: + if not args.gersemi_config.exists(): + print( + f"Error: gersemi config does not exist: {args.gersemi_config}", + file=sys.stderr, + ) + return 2 + gersemi_config_path = str(args.gersemi_config) + else: + print( + "Warning: --gersemi-config not supplied; gersemi will run with defaults.", + file=sys.stderr, + ) + + print(f"sanitize-history: rewriting commits in {args.repo}", file=sys.stderr) + print(f" clang-format: {args.clang_format_bin}", file=sys.stderr) + print(f" black: {args.black_bin}", file=sys.stderr) + print(f" gersemi: {args.gersemi_bin}", file=sys.stderr) + print(f" style file: {args.clang_format_style}", file=sys.stderr) + print(f" gersemi config: {gersemi_config_path or '(default)'}", file=sys.stderr) + print(f" log dir: {args.log_dir}", file=sys.stderr) + + os.chdir(args.repo) + + sanitizer = HistorySanitizer( + clang_format_bin=args.clang_format_bin, + black_bin=args.black_bin, + gersemi_bin=args.gersemi_bin, + gersemi_config=gersemi_config_path, + log_dir=args.log_dir, + ) + + filter_args = fr.FilteringOptions.parse_args(["--force"]) + repo_filter = fr.RepoFilter( + filter_args, + blob_callback=sanitizer.blob_callback, + commit_callback=sanitizer.commit_callback, + ) + + try: + repo_filter.run() + finally: + sanitizer.close() + + kc = sanitizer.kind_counts + print( + f"\nsanitize-history complete:\n" + f" commits walked: {sanitizer.commit_count}\n" + f" prefix auto-prepended: {sanitizer.prefix_changes}\n" + f" subjects truncated: {sanitizer.subject_truncations}\n" + f" blank lines inserted: {sanitizer.subject_blank_inserts}\n" + f" exec-bits cleared: {sanitizer.mode_normalizations}\n" + f" README.rst->.md fixes: {sanitizer.readme_ref_patches}\n" + f" dynamic DESCRIPTION rm:{sanitizer.dynamic_description_patches}\n" + f" standalone-guard rm: {sanitizer.standalone_guard_patches}\n" + f" cmake_min_required rm: {sanitizer.cmake_min_required_drops}\n" + f" blobs scanned: {sanitizer.blob_count}\n" + f" blobs reformatted: {sanitizer.format_changes}\n" + f" by-kind sniff: " + f"cxx={kc.get('cxx', 0)} " + f"py={kc.get('py', 0)} " + f"cmake={kc.get('cmake', 0)} " + f"text={kc.get('text', 0)} " + f"skip={sanitizer.blob_count - sum(kc.values())}\n" + f" logs: {args.log_dir}/", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOFDF.list b/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOFDF.list new file mode 100644 index 000000000000..7a032df7a29e --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOFDF.list @@ -0,0 +1,24 @@ +# IOFDF — files that migrate into ITK at Modules/IO/IOFDF/ +# +# Format: one path per line, relative to the upstream repo root. +# Globs supported via filter-repo --paths-from-file. +# Lines starting with '#' are comments; blank lines ignored. + +# Public headers +include + +# Implementation sources +src + +# Tests + fixtures +test + +# Python wrapping +wrapping + +# CMake / module metadata +CMakeLists.txt +itk-module.cmake + +# License (always migrates with the code) +LICENSE diff --git a/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOMeshMZ3.list b/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOMeshMZ3.list new file mode 100644 index 000000000000..b82b11dbb195 --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOMeshMZ3.list @@ -0,0 +1,24 @@ +# IOMeshMZ3 — files that migrate into ITK at Modules/IO/IOMeshMZ3/ +# +# Format: one path per line, relative to the upstream repo root. +# Globs supported via filter-repo --paths-from-file. +# Lines starting with '#' are comments; blank lines ignored. + +# Public headers +include + +# Implementation sources +src + +# Tests + fixtures +test + +# Python wrapping +wrapping + +# CMake / module metadata +CMakeLists.txt +itk-module.cmake + +# License (always migrates with the code) +LICENSE diff --git a/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOMeshSTL.list b/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOMeshSTL.list new file mode 100644 index 000000000000..24019b6adfc7 --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/whitelists/IOMeshSTL.list @@ -0,0 +1,25 @@ +# IOMeshSTL — files that migrate into ITK at Modules/IO/MeshSTL/ +# +# Format: one path per line, relative to the upstream repo root. +# Globs supported via filter-repo --paths-from-file (treated as +# path prefixes; trailing /** matches everything beneath the dir). +# Lines starting with '#' are comments; blank lines ignored. + +# Public headers +include + +# Implementation sources +src + +# Tests + fixtures +test + +# Python wrapping +wrapping + +# CMake / module metadata +CMakeLists.txt +itk-module.cmake + +# License (always migrates with the code) +LICENSE diff --git a/Utilities/Maintenance/RemoteModuleIngest/whitelists/PolarTransform.list b/Utilities/Maintenance/RemoteModuleIngest/whitelists/PolarTransform.list new file mode 100644 index 000000000000..60e6f4ed84ff --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/whitelists/PolarTransform.list @@ -0,0 +1,21 @@ +# PolarTransform — files that migrate into ITK at Modules/Filtering/PolarTransform/ +# +# Format: one path per line, relative to the upstream repo root. +# Globs supported via filter-repo --paths-from-file. +# Lines starting with '#' are comments; blank lines ignored. + +# Public headers (PolarTransform is header-only; no src/) +include + +# Tests + fixtures +test + +# Python wrapping +wrapping + +# CMake / module metadata +CMakeLists.txt +itk-module.cmake + +# License (always migrates with the code) +LICENSE diff --git a/Utilities/Maintenance/RemoteModuleIngest/whitelists/SplitComponents.list b/Utilities/Maintenance/RemoteModuleIngest/whitelists/SplitComponents.list new file mode 100644 index 000000000000..4929b68cc47f --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/whitelists/SplitComponents.list @@ -0,0 +1,21 @@ +# SplitComponents — files that migrate into ITK at Modules/Filtering/SplitComponents/ +# +# Format: one path per line, relative to the upstream repo root. +# Globs supported via filter-repo --paths-from-file. +# Lines starting with '#' are comments; blank lines ignored. + +# Public headers (SplitComponents is header-only; no src/) +include + +# Tests + fixtures +test + +# Python wrapping +wrapping + +# CMake / module metadata +CMakeLists.txt +itk-module.cmake + +# License (always migrates with the code) +LICENSE diff --git a/Utilities/Maintenance/RemoteModuleIngest/whitelists/SubdivisionQuadEdgeMeshFilter.list b/Utilities/Maintenance/RemoteModuleIngest/whitelists/SubdivisionQuadEdgeMeshFilter.list new file mode 100644 index 000000000000..89024359bfde --- /dev/null +++ b/Utilities/Maintenance/RemoteModuleIngest/whitelists/SubdivisionQuadEdgeMeshFilter.list @@ -0,0 +1,20 @@ +# SubdivisionQuadEdgeMeshFilter — files that migrate into ITK at +# Modules/Filtering/SubdivisionQuadEdgeMeshFilter/ +# +# Format: one path per line, relative to the upstream repo root. +# Globs supported via filter-repo --paths-from-file (treated as +# path prefixes; trailing /** matches everything beneath the dir). +# Lines starting with '#' are comments; blank lines ignored. + +# Public headers +include + +# Tests + fixtures +test + +# CMake / module metadata +CMakeLists.txt +itk-module.cmake + +# License (always migrates with the code) +LICENSE diff --git a/pyproject.toml b/pyproject.toml index ad9c7f162ca4..619524a7ece4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ cmd = '''cmake -DModule_IOFDF:BOOL=ON -DModule_MorphologicalContourInterpolation:BOOL=ON -DModule_RLEImage:BOOL=ON + -DModule_SubdivisionQuadEdgeMeshFilter:BOOL=ON -DITK_COMPUTER_MEMORY_SIZE:STRING=11 -DModule_StructuralSimilarity:BOOL=ON''' description = "Configure ITK for CI (with ccache compiler launcher)"