From 96d105769a640b6029ad0f3e6881eefe5b59473b Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Thu, 4 Dec 2025 10:50:50 +0100 Subject: [PATCH 01/12] Use Delete() method for occ handles --- src/SWIG_files/common/OccHandle.i | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SWIG_files/common/OccHandle.i b/src/SWIG_files/common/OccHandle.i index 9e88b0f28..ea52e6972 100644 --- a/src/SWIG_files/common/OccHandle.i +++ b/src/SWIG_files/common/OccHandle.i @@ -347,7 +347,7 @@ WRAP_OCC_TRANSIENT(const, TYPE) handle_deletion_count++; #endif if($this->DecrementRefCounter() == 0) { - delete $this; + $this->Delete(); } } %} From 49c289871882dbc20c41a355e3d7681654d06991 Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Thu, 4 Dec 2025 17:53:44 +0100 Subject: [PATCH 02/12] Add OpenGL Core Profile API to Display3d Expose OpenGl_Caps options via new Display3d methods: - SetCoreProfileEnabled(bool) - convenience for Core Profile - SetContextCompatible(bool) - control compatibility mode - SetFfpEnable(bool) - enable/disable fixed-function pipeline - SetBuffersNoSwap(bool) - for external GL context management On macOS, compatibility profile limits OpenGL to 2.1/GLSL 1.20. Core Profile enables 3.2+/GLSL 1.50+ required for Qt6 integration. Must be called BEFORE Init() to take effect. --- src/Visualization/Display3d.cpp | 35 +++++++++++++++++++++++++++++++ src/Visualization/Visualization.h | 7 +++++++ src/Visualization/Visualization.i | 8 +++++++ 3 files changed, 50 insertions(+) diff --git a/src/Visualization/Display3d.cpp b/src/Visualization/Display3d.cpp index c4eb8c180..d840dd3df 100644 --- a/src/Visualization/Display3d.cpp +++ b/src/Visualization/Display3d.cpp @@ -283,3 +283,38 @@ void Display3d::Test() myV3dView->ZFitAll(); myV3dView->FitAll(); } + +void Display3d::SetCoreProfileEnabled(Standard_Boolean theEnabled) +{ + // Request OpenGL Core Profile instead of compatibility profile. + // On macOS, compatibility profile is limited to OpenGL 2.1 / GLSL 1.20. + // Core Profile enables OpenGL 3.2+ / GLSL 1.50+ which is required for + // Qt6 scene graph integration and modern shader features. + // Must be called BEFORE Init() to take effect. + GetGraphicDriver()->ChangeOptions().contextCompatible = !theEnabled; + GetGraphicDriver()->ChangeOptions().ffpEnable = !theEnabled; +} + +void Display3d::SetContextCompatible(Standard_Boolean theCompatible) +{ + // Set whether to request backward-compatible OpenGL context. + // When false, requests Core Profile (OpenGL 3.2+). + // Must be called BEFORE Init() to take effect. + GetGraphicDriver()->ChangeOptions().contextCompatible = theCompatible; +} + +void Display3d::SetFfpEnable(Standard_Boolean theEnabled) +{ + // Enable/disable fixed-function pipeline. + // Should be disabled when using Core Profile. + // Must be called BEFORE Init() to take effect. + GetGraphicDriver()->ChangeOptions().ffpEnable = theEnabled; +} + +void Display3d::SetBuffersNoSwap(Standard_Boolean theNoSwap) +{ + // Set whether to skip buffer swapping (for external GL context management). + // Useful when integrating with Qt Quick or other frameworks that manage + // their own buffer swapping. + GetGraphicDriver()->ChangeOptions().buffersNoSwap = theNoSwap; +} diff --git a/src/Visualization/Visualization.h b/src/Visualization/Visualization.h index 3f6756777..409de9f16 100644 --- a/src/Visualization/Visualization.h +++ b/src/Visualization/Visualization.h @@ -68,6 +68,13 @@ class Display3d Standard_Boolean ToReverseStereo); Standard_EXPORT void EnableVBO(); Standard_EXPORT void DisableVBO(); + + // OpenGL context configuration - must be called BEFORE Init() + Standard_EXPORT void SetCoreProfileEnabled(Standard_Boolean theEnabled); + Standard_EXPORT void SetContextCompatible(Standard_Boolean theCompatible); + Standard_EXPORT void SetFfpEnable(Standard_Boolean theEnabled); + Standard_EXPORT void SetBuffersNoSwap(Standard_Boolean theNoSwap); + Standard_EXPORT Handle_V3d_View& GetView() {return myV3dView;}; Standard_EXPORT Handle_V3d_Viewer& GetViewer() {return myV3dViewer;}; Standard_EXPORT Handle_Graphic3d_Camera& GetCamera() {return myGraphic3dCamera;}; diff --git a/src/Visualization/Visualization.i b/src/Visualization/Visualization.i index bb11c712b..db02cae5b 100644 --- a/src/Visualization/Visualization.i +++ b/src/Visualization/Visualization.i @@ -81,6 +81,14 @@ class Display3d { bool SetSize(int size_x, int size_y); %feature("autodoc", "1"); bool IsOffscreen(); + %feature("autodoc", "1"); + void SetCoreProfileEnabled(bool theEnabled); + %feature("autodoc", "1"); + void SetContextCompatible(bool theCompatible); + %feature("autodoc", "1"); + void SetFfpEnable(bool theEnabled); + %feature("autodoc", "1"); + void SetBuffersNoSwap(bool theNoSwap); }; %extend Display3d { From 8bfd75b6fecd80accf99f9e767a8911e1f7c3ece Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Thu, 4 Dec 2025 20:51:13 +0100 Subject: [PATCH 03/12] Add SetSRGBDisabled() to disable sRGB framebuffer - exposes OpenGl_Caps::sRGBDisable via Display3d API - fixes GL_SRGB8_ALPHA8 texture error on macOS Core Profile - must call before Init()/InitOffscreen() --- src/Visualization/Display3d.cpp | 10 ++++++++++ src/Visualization/Visualization.h | 1 + src/Visualization/Visualization.i | 2 ++ 3 files changed, 13 insertions(+) diff --git a/src/Visualization/Display3d.cpp b/src/Visualization/Display3d.cpp index d840dd3df..693e7cf15 100644 --- a/src/Visualization/Display3d.cpp +++ b/src/Visualization/Display3d.cpp @@ -318,3 +318,13 @@ void Display3d::SetBuffersNoSwap(Standard_Boolean theNoSwap) // their own buffer swapping. GetGraphicDriver()->ChangeOptions().buffersNoSwap = theNoSwap; } + +void Display3d::SetSRGBDisabled(Standard_Boolean theDisabled) +{ + // Disable sRGB rendering (OFF by default in OCCT). + // When enabled, OCCT won't attempt to create sRGB framebuffers. + // Useful for macOS where GL_SRGB8_ALPHA8 format may not be supported + // in certain contexts. + // Must be called BEFORE Init()/InitOffscreen() to take effect. + GetGraphicDriver()->ChangeOptions().sRGBDisable = theDisabled; +} diff --git a/src/Visualization/Visualization.h b/src/Visualization/Visualization.h index 409de9f16..35e91699f 100644 --- a/src/Visualization/Visualization.h +++ b/src/Visualization/Visualization.h @@ -74,6 +74,7 @@ class Display3d Standard_EXPORT void SetContextCompatible(Standard_Boolean theCompatible); Standard_EXPORT void SetFfpEnable(Standard_Boolean theEnabled); Standard_EXPORT void SetBuffersNoSwap(Standard_Boolean theNoSwap); + Standard_EXPORT void SetSRGBDisabled(Standard_Boolean theDisabled); Standard_EXPORT Handle_V3d_View& GetView() {return myV3dView;}; Standard_EXPORT Handle_V3d_Viewer& GetViewer() {return myV3dViewer;}; diff --git a/src/Visualization/Visualization.i b/src/Visualization/Visualization.i index db02cae5b..b36dfe334 100644 --- a/src/Visualization/Visualization.i +++ b/src/Visualization/Visualization.i @@ -89,6 +89,8 @@ class Display3d { void SetFfpEnable(bool theEnabled); %feature("autodoc", "1"); void SetBuffersNoSwap(bool theNoSwap); + %feature("autodoc", "1"); + void SetSRGBDisabled(bool theDisabled); }; %extend Display3d { From d30520d99d1ce0b94a9e065eb2e8e09c9497a3f7 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 28 Dec 2025 06:12:32 +0100 Subject: [PATCH 04/12] Improve ShapeTesselator --- src/Tesselator/ShapeTesselator.cpp | 282 ++++++++++++++++++----------- src/Tesselator/ShapeTesselator.h | 4 +- 2 files changed, 178 insertions(+), 108 deletions(-) diff --git a/src/Tesselator/ShapeTesselator.cpp b/src/Tesselator/ShapeTesselator.cpp index c6b0dc858..1a42afd54 100644 --- a/src/Tesselator/ShapeTesselator.cpp +++ b/src/Tesselator/ShapeTesselator.cpp @@ -26,6 +26,11 @@ #include #include #include +#include + +#ifdef _OPENMP +#include +#endif // OpenCASCADE includes #include @@ -76,9 +81,9 @@ Standard_Integer ShapeTesselator::Edge::size() const noexcept { // ======================================================================== ShapeTesselator::ShapeTesselator(const TopoDS_Shape& aShape) - : computed(false), myShape(aShape) { + : computed(false), use_parallel(false), myShape(aShape) { ComputeDefaultDeviation(); - + // Reserve memory for face and edge collections based on estimates face_list.reserve(100); // Initial estimate edge_list.reserve(200); // Initial estimate @@ -122,18 +127,20 @@ void ShapeTesselator::Tessellate(bool compute_edges, float mesh_quality, bool pa throw std::invalid_argument("The mesh quality must be greater than 0"); } + use_parallel = parallel; + // Clean and tessellate the shape BRepTools::Clean(myShape); BRepMesh_IncrementalMesh(myShape, myDeviation * mesh_quality, false, 0.5f * mesh_quality, parallel); - // Estimate number of faces for pre-allocation - Standard_Integer face_count = 0; + // Collect faces for processing + std::vector faces; for (TopExp_Explorer exp(myShape, TopAbs_FACE); exp.More(); exp.Next()) { - ++face_count; + faces.push_back(TopoDS::Face(exp.Current())); } - face_list.reserve(face_count); + face_list.reserve(faces.size()); - ProcessFaces(); + ProcessFaces(faces); JoinPrimitives(); if (compute_edges) { @@ -141,21 +148,56 @@ void ShapeTesselator::Tessellate(bool compute_edges, float mesh_quality, bool pa } } -void ShapeTesselator::ProcessFaces() { - for (TopExp_Explorer exp_face(myShape, TopAbs_FACE); exp_face.More(); exp_face.Next()) { - TopLoc_Location location; - const auto& face = TopoDS::Face(exp_face.Current()); - auto triangulation = BRep_Tool::Triangulation(face, location); +void ShapeTesselator::ProcessFaces(const std::vector& faces) { + const auto num_faces = static_cast(faces.size()); - if (triangulation.IsNull()) { - continue; +#ifdef _OPENMP + if (use_parallel && num_faces > 1) { + // Parallel processing: create face data in parallel, then collect results + std::vector> local_results(num_faces); + + #pragma omp parallel for schedule(dynamic) + for (Standard_Integer i = 0; i < num_faces; ++i) { + TopLoc_Location location; + const auto& face = faces[i]; + auto triangulation = BRep_Tool::Triangulation(face, location); + + if (triangulation.IsNull()) { + continue; + } + + auto face_data = std::make_unique(); + ProcessSingleFace(face, triangulation, location, *face_data); + + if (face_data->number_of_triangles > 0) { + local_results[i] = std::move(face_data); + } } - auto face_data = std::make_unique(); - ProcessSingleFace(face, triangulation, location, *face_data); - - if (face_data->number_of_triangles > 0) { - face_list.push_back(std::move(face_data)); + // Collect non-null results + for (auto& result : local_results) { + if (result) { + face_list.push_back(std::move(result)); + } + } + } else +#endif + { + // Sequential processing + for (const auto& face : faces) { + TopLoc_Location location; + auto triangulation = BRep_Tool::Triangulation(face, location); + + if (triangulation.IsNull()) { + continue; + } + + auto face_data = std::make_unique(); + ProcessSingleFace(face, triangulation, location, *face_data); + + if (face_data->number_of_triangles > 0) { + face_list.push_back(std::move(face_data)); + } } } } @@ -181,8 +223,8 @@ void ShapeTesselator::ProcessSingleFace(const TopoDS_Face& face, face_data.vertex_coords[idx + 2] = static_cast(point.Z()); } - // Process normals if available - if (triangulation->HasUVNodes()) { + // Process normals - prefer pre-computed normals, fallback to UV computation + if (triangulation->HasNormals() || triangulation->HasUVNodes()) { ProcessNormals(face, triangulation, face_data); } else { ++face_data.number_of_invalid_normals; @@ -195,22 +237,46 @@ void ShapeTesselator::ProcessSingleFace(const TopoDS_Face& face, void ShapeTesselator::ProcessNormals(const TopoDS_Face& face, const Handle(Poly_Triangulation)& triangulation, Face& face_data) { - + const auto nb_nodes = triangulation->NbNodes(); - BRepGProp_Face prop(face); - face_data.normal_coords.resize(nb_nodes * 3); face_data.number_of_normals = nb_nodes; - + const bool reverse_orientation = (face.Orientation() == TopAbs_INTERNAL); - + + // Use pre-computed normals from triangulation when available (OCC 7.6+) + // This is much faster than recomputing via BRepGProp_Face::Normal() + if (triangulation->HasNormals()) { + for (Standard_Integer i = 1; i <= nb_nodes; ++i) { + auto normal = triangulation->Normal(i); + + if (reverse_orientation) { + normal.Reverse(); + } + + const auto idx = (i - 1) * 3; + face_data.normal_coords[idx] = static_cast(normal.X()); + face_data.normal_coords[idx + 1] = static_cast(normal.Y()); + face_data.normal_coords[idx + 2] = static_cast(normal.Z()); + } + return; + } + + // Fallback: compute normals from UV coordinates (slower path) + if (!triangulation->HasUVNodes()) { + ++face_data.number_of_invalid_normals; + return; + } + + BRepGProp_Face prop(face); + for (Standard_Integer i = 1; i <= nb_nodes; ++i) { const auto& uv_point = triangulation->UVNode(i); gp_Pnt point; gp_Vec normal; - + prop.Normal(uv_point.X(), uv_point.Y(), point, normal); - + if (normal.SquareMagnitude() > Precision::SquareConfusion()) { normal.Normalize(); if (reverse_orientation) { @@ -219,7 +285,7 @@ void ShapeTesselator::ProcessNormals(const TopoDS_Face& face, } else { normal.SetCoord(0., 0., 0.); } - + const auto idx = (i - 1) * 3; face_data.normal_coords[idx] = static_cast(normal.X()); face_data.normal_coords[idx + 1] = static_cast(normal.Y()); @@ -230,22 +296,26 @@ void ShapeTesselator::ProcessNormals(const TopoDS_Face& face, void ShapeTesselator::ProcessTriangles(const TopoDS_Face& face, const Handle(Poly_Triangulation)& triangulation, Face& face_data) { - + const auto nb_triangles = triangulation->NbTriangles(); const auto is_reversed = (face.Orientation() == TopAbs_REVERSED); - - face_data.triangle_indices.reserve(nb_triangles * 3); - + + // Pre-allocate exact size + face_data.triangle_indices.resize(nb_triangles * 3); + face_data.number_of_triangles = nb_triangles; + for (Standard_Integer i = 1; i <= nb_triangles; ++i) { Standard_Integer n1, n2, n3; triangulation->Triangle(i).Get(n1, n2, n3); - + if (is_reversed) { std::swap(n2, n3); } - - face_data.triangle_indices.insert(face_data.triangle_indices.end(), {n1, n2, n3}); - ++face_data.number_of_triangles; + + const auto base_idx = (i - 1) * 3; + face_data.triangle_indices[base_idx] = n1; + face_data.triangle_indices[base_idx + 1] = n2; + face_data.triangle_indices[base_idx + 2] = n3; } } @@ -339,18 +409,21 @@ bool ShapeTesselator::ProcessSingleEdge(const TopoDS_Edge& edge, if (!location.IsIdentity()) { transform = location.Transformation(); } - + const auto& nodes = poly_3d->Nodes(); const auto nb_nodes = poly_3d->NbNodes(); - - edge_data.reserve(nb_nodes); - + + // Pre-allocate exact size + edge_data.vertex_coords.resize(nb_nodes * 3); + for (Standard_Integer i = 1; i <= nb_nodes; ++i) { auto vertex = nodes(i); vertex.Transform(transform); - - edge_data.vertex_coords.insert(edge_data.vertex_coords.end(), - {static_cast(vertex.X()), static_cast(vertex.Y()), static_cast(vertex.Z())}); + + const auto idx = (i - 1) * 3; + edge_data.vertex_coords[idx] = static_cast(vertex.X()); + edge_data.vertex_coords[idx + 1] = static_cast(vertex.Y()); + edge_data.vertex_coords[idx + 2] = static_cast(vertex.Z()); } return true; } @@ -371,20 +444,23 @@ bool ShapeTesselator::ProcessSingleEdge(const TopoDS_Edge& edge, if (!location.IsIdentity()) { transform = location.Transformation(); } - + const auto& indices = poly_on_tri->Nodes(); const auto nb_nodes = poly_on_tri->NbNodes(); - - edge_data.reserve(nb_nodes); - + + // Pre-allocate exact size + edge_data.vertex_coords.resize(nb_nodes * 3); + for (Standard_Integer i = 1; i <= nb_nodes; ++i) { auto vertex = face_triangulation->Node(indices(i)); vertex.Transform(transform); - - edge_data.vertex_coords.insert(edge_data.vertex_coords.end(), - {static_cast(vertex.X()), static_cast(vertex.Y()), static_cast(vertex.Z())}); + + const auto idx = (i - 1) * 3; + edge_data.vertex_coords[idx] = static_cast(vertex.X()); + edge_data.vertex_coords[idx + 1] = static_cast(vertex.Y()); + edge_data.vertex_coords[idx + 2] = static_cast(vertex.Z()); } - + return true; } @@ -442,43 +518,41 @@ const float* ShapeTesselator::NormalsList() const { std::vector ShapeTesselator::GetVerticesPositionAsTuple() const { if (!computed) return {}; - - std::vector result; - result.reserve(tot_triangle_count * 9); // 3 vertices * 3 coords - + + const auto total_floats = tot_triangle_count * 9; // 3 vertices * 3 coords + std::vector result(total_floats); + + Standard_Integer out_idx = 0; for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { const auto base_idx = i * 3; for (int j = 0; j < 3; ++j) { const auto vertex_idx = consolidated_triangle_indices[base_idx + j] * 3; - result.insert(result.end(), { - consolidated_vertices[vertex_idx], - consolidated_vertices[vertex_idx + 1], - consolidated_vertices[vertex_idx + 2] - }); + result[out_idx++] = consolidated_vertices[vertex_idx]; + result[out_idx++] = consolidated_vertices[vertex_idx + 1]; + result[out_idx++] = consolidated_vertices[vertex_idx + 2]; } } - + return result; } std::vector ShapeTesselator::GetNormalsAsTuple() const { if (!computed) return {}; - - std::vector result; - result.reserve(tot_triangle_count * 9); - + + const auto total_floats = tot_triangle_count * 9; + std::vector result(total_floats); + + Standard_Integer out_idx = 0; for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { const auto base_idx = i * 3; for (int j = 0; j < 3; ++j) { const auto normal_idx = consolidated_triangle_indices[base_idx + j] * 3; - result.insert(result.end(), { - consolidated_normals[normal_idx], - consolidated_normals[normal_idx + 1], - consolidated_normals[normal_idx + 2] - }); + result[out_idx++] = consolidated_normals[normal_idx]; + result[out_idx++] = consolidated_normals[normal_idx + 1]; + result[out_idx++] = consolidated_normals[normal_idx + 2]; } } - + return result; } @@ -557,15 +631,14 @@ void ShapeTesselator::ObjGetTriangle(Standard_Integer trianglenum, Standard_Inte // ======================================================================== namespace { - //! Format float number with epsilon handling - std::string formatFloatNumber(float f) { - const float epsilon = 1e-3f; - std::ostringstream formatted_float; + //! Format float number with epsilon handling directly to stream + inline void writeFloat(std::ostringstream& out, float f) { + constexpr float epsilon = 1e-3f; if (std::abs(f) < epsilon) { - f = 0.0f; + out << "0"; + } else { + out << f; } - formatted_float << f; - return formatted_float.str(); } } @@ -632,38 +705,33 @@ std::string ShapeTesselator::ExportShapeToThreejsJSONString(const char* shape_fu std::string ShapeTesselator::ExportShapeToX3DTriangleSet() const { if (!computed) return ""; - - std::ostringstream str_ifs, str_vertices, str_normals; - std::vector vertices_idx(3); - std::vector normals_idx(3); - - // Process triangles and build vertex/normal strings + + std::ostringstream str_vertices, str_normals; + + // Process triangles and build vertex/normal strings directly for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { - ObjGetTriangle(i, vertices_idx.data(), normals_idx.data()); - - // Process vertices - for (int j = 0; j < 3; ++j) { - const auto idx = vertices_idx[j]; - str_vertices << formatFloatNumber(consolidated_vertices[idx]) << " "; - str_vertices << formatFloatNumber(consolidated_vertices[idx + 1]) << " "; - str_vertices << formatFloatNumber(consolidated_vertices[idx + 2]) << " "; - } - - // Process normals + const auto base_idx = i * 3; + + // Process all 3 vertices of the triangle for (int j = 0; j < 3; ++j) { - const auto idx = normals_idx[j]; - str_normals << formatFloatNumber(consolidated_normals[idx]) << " "; - str_normals << formatFloatNumber(consolidated_normals[idx + 1]) << " "; - str_normals << formatFloatNumber(consolidated_normals[idx + 2]) << " "; + const auto vertex_idx = consolidated_triangle_indices[base_idx + j] * 3; + writeFloat(str_vertices, consolidated_vertices[vertex_idx]); str_vertices << ' '; + writeFloat(str_vertices, consolidated_vertices[vertex_idx + 1]); str_vertices << ' '; + writeFloat(str_vertices, consolidated_vertices[vertex_idx + 2]); str_vertices << ' '; + + writeFloat(str_normals, consolidated_normals[vertex_idx]); str_normals << ' '; + writeFloat(str_normals, consolidated_normals[vertex_idx + 1]); str_normals << ' '; + writeFloat(str_normals, consolidated_normals[vertex_idx + 2]); str_normals << ' '; } } - - str_ifs << "\n"; - str_ifs << "\n"; - str_ifs << "\n"; - str_ifs << "\n"; - - return str_ifs.str(); + + std::ostringstream result; + result << "\n"; + result << "\n"; + result << "\n"; + result << "\n"; + + return result.str(); } void ShapeTesselator::ExportShapeToX3D(const char* filename, int diffR, int diffG, int diffB) { diff --git a/src/Tesselator/ShapeTesselator.h b/src/Tesselator/ShapeTesselator.h index 1efb01939..b171fca4e 100644 --- a/src/Tesselator/ShapeTesselator.h +++ b/src/Tesselator/ShapeTesselator.h @@ -68,6 +68,7 @@ class ShapeTesselator { private: // Internal state bool computed; //!< Whether tessellation has been computed + bool use_parallel; //!< Whether to use parallel processing TopoDS_Shape myShape; //!< The shape to tessellate Standard_Real myDeviation; //!< Tessellation deviation parameter @@ -196,7 +197,8 @@ class ShapeTesselator { void Tessellate(bool compute_edges, float mesh_quality, bool parallel); //! Process all faces in the shape - void ProcessFaces(); + //! @param faces Vector of faces to process + void ProcessFaces(const std::vector& faces); //! Process a single face //! @param face The face to process From 4db85aa834730817d93421c87a2ce14be53a8ee8 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 28 Dec 2025 05:55:49 +0100 Subject: [PATCH 05/12] Bump occt to 7.9.3 and swig to 4.4.1 --- CMakeLists.txt | 6 +++--- ci/conda/meta.yaml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d29f1d79d..08bd62772 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ project(PYTHONOCC) # set pythonOCC version set(PYTHONOCC_VERSION_MAJOR 7) set(PYTHONOCC_VERSION_MINOR 9) -set(PYTHONOCC_VERSION_PATCH 1) +set(PYTHONOCC_VERSION_PATCH 3) # Empty for official releases, set to -dev, -rc1, etc for development releases set(PYTHONOCC_VERSION_DEVEL -dev) @@ -30,7 +30,7 @@ set(PYTHONOCC_VERSION_DEVEL -dev) # set OCCT version set(OCCT_VERSION_MAJOR 7) set(OCCT_VERSION_MINOR 9) -set(OCCT_VERSION_PATCH 1) +set(OCCT_VERSION_PATCH 3) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake ${CMAKE_MODULE_PATH}) @@ -110,7 +110,7 @@ message(STATUS "Python library release: ${Python3_LIBRARY_RELEASE}") ######## # SWIG # ######## -find_package(SWIG 4.2.1...4.3.1 REQUIRED) +find_package(SWIG 4.2.1...4.4.1 REQUIRED) message(STATUS "SWIG version found: ${SWIG_VERSION}") include(${SWIG_USE_FILE}) set(SWIG_FILES_PATH src/SWIG_files/wrapper) diff --git a/ci/conda/meta.yaml b/ci/conda/meta.yaml index 1a9afbf22..cb272cf1b 100644 --- a/ci/conda/meta.yaml +++ b/ci/conda/meta.yaml @@ -1,4 +1,4 @@ -{% set version = "7.9.1" %} +{% set version = "7.9.3" %} package: name: pythonocc-core @@ -22,15 +22,15 @@ requirements: - {{ cdt('libxi-devel') }} # [linux] - ninja - cmake - - swig ==4.3.1 + - swig ==4.4.1 host: - python {{ python }} - - occt ==7.9.1 + - occt ==7.9.3 - numpy >=1.17 run: - - occt ==7.9.1 + - occt ==7.9.3 - numpy >=1.17 test: From 2f8f1a7d99312e8b3e81d0bb2adab9b1e717d37b Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Wed, 31 Dec 2025 05:25:40 +0100 Subject: [PATCH 06/12] Fix memory leak in TopoDS_Shape output typemaps The TopoDS_Shape output typemaps were using direct return statements (Py_RETURN_NONE and return resultobj) which bypassed SWIG's cleanup code for input arguments. This caused memory leaks when functions taking std::string parameters returned TopoDS_Shape, as the temporary std::string objects were never freed. Changed to use $result assignment instead of direct returns, allowing SWIG to generate proper freearg cleanup code that calls "if (SWIG_IsNewObj(res)) delete arg" for temporary std::string objects. --- src/SWIG_files/common/FunctionTransformers.i | 226 ++++++++++--------- 1 file changed, 114 insertions(+), 112 deletions(-) diff --git a/src/SWIG_files/common/FunctionTransformers.i b/src/SWIG_files/common/FunctionTransformers.i index bcf989643..837fa4d86 100644 --- a/src/SWIG_files/common/FunctionTransformers.i +++ b/src/SWIG_files/common/FunctionTransformers.i @@ -138,128 +138,130 @@ Standard_Boolean & function transformation %typemap(out) TopoDS_Shape { TopoDS_Shape* sh = &$1; if (!sh || sh->IsNull()) { - Py_RETURN_NONE; + // Use $result instead of Py_RETURN_NONE to allow SWIG cleanup code to run + $result = Py_None; + Py_INCREF(Py_None); } - PyObject *resultobj = nullptr; - - switch (sh->ShapeType()) - { - case TopAbs_COMPOUND: { - TopoDS_Compound* ptr = new TopoDS_Compound(TopoDS::Compound(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Compound, SWIG_POINTER_OWN | 0); - if (!resultobj) delete ptr; - break; - } - case TopAbs_COMPSOLID: { - TopoDS_CompSolid* ptr = new TopoDS_CompSolid(TopoDS::CompSolid(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_CompSolid, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_SOLID: { - TopoDS_Solid* ptr = new TopoDS_Solid(TopoDS::Solid(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Solid, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_SHELL: { - TopoDS_Shell* ptr = new TopoDS_Shell(TopoDS::Shell(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Shell, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_FACE: { - TopoDS_Face* ptr = new TopoDS_Face(TopoDS::Face(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Face, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_WIRE: { - TopoDS_Wire* ptr = new TopoDS_Wire(TopoDS::Wire(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Wire, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_EDGE: { - TopoDS_Edge* ptr = new TopoDS_Edge(TopoDS::Edge(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Edge, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_VERTEX: { - TopoDS_Vertex* ptr = new TopoDS_Vertex(TopoDS::Vertex(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Vertex, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - default: - break; + else { + switch (sh->ShapeType()) + { + case TopAbs_COMPOUND: { + TopoDS_Compound* ptr = new TopoDS_Compound(TopoDS::Compound(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Compound, SWIG_POINTER_OWN | 0); + if (!$result) delete ptr; + break; + } + case TopAbs_COMPSOLID: { + TopoDS_CompSolid* ptr = new TopoDS_CompSolid(TopoDS::CompSolid(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_CompSolid, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_SOLID: { + TopoDS_Solid* ptr = new TopoDS_Solid(TopoDS::Solid(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Solid, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_SHELL: { + TopoDS_Shell* ptr = new TopoDS_Shell(TopoDS::Shell(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Shell, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_FACE: { + TopoDS_Face* ptr = new TopoDS_Face(TopoDS::Face(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Face, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_WIRE: { + TopoDS_Wire* ptr = new TopoDS_Wire(TopoDS::Wire(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Wire, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_EDGE: { + TopoDS_Edge* ptr = new TopoDS_Edge(TopoDS::Edge(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Edge, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_VERTEX: { + TopoDS_Vertex* ptr = new TopoDS_Vertex(TopoDS::Vertex(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Vertex, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + default: + break; + } } - return resultobj; } // Return TopoDS_Shapes by copy, as we could get lifetimes errors %typemap(out) const TopoDS_Shape& { TopoDS_Shape* sh = $1; if (!sh || sh->IsNull()) { - Py_RETURN_NONE; + // Use $result instead of Py_RETURN_NONE to allow SWIG cleanup code to run + $result = Py_None; + Py_INCREF(Py_None); } - PyObject *resultobj = nullptr; - - switch (sh->ShapeType()) - { - case TopAbs_COMPOUND: { - TopoDS_Compound* ptr = new TopoDS_Compound(TopoDS::Compound(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Compound, SWIG_POINTER_OWN | 0); - if (!resultobj) delete ptr; - break; - } - case TopAbs_COMPSOLID: { - TopoDS_CompSolid* ptr = new TopoDS_CompSolid(TopoDS::CompSolid(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_CompSolid, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_SOLID: { - TopoDS_Solid* ptr = new TopoDS_Solid(TopoDS::Solid(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Solid, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_SHELL: { - TopoDS_Shell* ptr = new TopoDS_Shell(TopoDS::Shell(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Shell, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_FACE: { - TopoDS_Face* ptr = new TopoDS_Face(TopoDS::Face(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Face, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_WIRE: { - TopoDS_Wire* ptr = new TopoDS_Wire(TopoDS::Wire(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Wire, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_EDGE: { - TopoDS_Edge* ptr = new TopoDS_Edge(TopoDS::Edge(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Edge, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - case TopAbs_VERTEX: { - TopoDS_Vertex* ptr = new TopoDS_Vertex(TopoDS::Vertex(*sh)); - resultobj = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Vertex, SWIG_POINTER_OWN | 0 ); - if (!resultobj) delete ptr; - break; - } - default: - break; + else { + switch (sh->ShapeType()) + { + case TopAbs_COMPOUND: { + TopoDS_Compound* ptr = new TopoDS_Compound(TopoDS::Compound(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Compound, SWIG_POINTER_OWN | 0); + if (!$result) delete ptr; + break; + } + case TopAbs_COMPSOLID: { + TopoDS_CompSolid* ptr = new TopoDS_CompSolid(TopoDS::CompSolid(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_CompSolid, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_SOLID: { + TopoDS_Solid* ptr = new TopoDS_Solid(TopoDS::Solid(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Solid, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_SHELL: { + TopoDS_Shell* ptr = new TopoDS_Shell(TopoDS::Shell(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Shell, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_FACE: { + TopoDS_Face* ptr = new TopoDS_Face(TopoDS::Face(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Face, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_WIRE: { + TopoDS_Wire* ptr = new TopoDS_Wire(TopoDS::Wire(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Wire, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_EDGE: { + TopoDS_Edge* ptr = new TopoDS_Edge(TopoDS::Edge(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Edge, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + case TopAbs_VERTEX: { + TopoDS_Vertex* ptr = new TopoDS_Vertex(TopoDS::Vertex(*sh)); + $result = SWIG_NewPointerObj(ptr, SWIGTYPE_p_TopoDS_Vertex, SWIG_POINTER_OWN | 0 ); + if (!$result) delete ptr; + break; + } + default: + break; + } } - return resultobj; } From f21bf74d53feaa2b546e7bb621ae041486134954 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sat, 14 Feb 2026 11:39:38 +0100 Subject: [PATCH 07/12] Bump to C++17, add Python 3.13/3.14 support, and fix conda build issues - Bump C++ standard from C++14 to C++17 - Add sysroot_linux-64 >= 2.28 for conda Linux builds (fixes timespec_get with GCC 15) - Add support for Python 3.13 and 3.14 - Fix OpenGL include path and guard Addons behind visualization check - Fix dylib warnings on macOS 10.13 - Fix installation directory discovery for py3.13 - Replace swig_link_libraries with target_link_libraries - Install GUI test dependencies on Windows only --- CMakeLists.txt | 63 +++++++++++++++++++++++++-------------------- azure-pipelines.yml | 42 ++++++++++++++++++++++++++++++ ci/conda/build.sh | 16 +++++------- ci/conda/meta.yaml | 13 +++++++--- conda-build.yml | 6 ++--- 5 files changed, 96 insertions(+), 44 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 08bd62772..ecc740191 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,8 +45,8 @@ if (POLICY CMP0086) cmake_policy(SET CMP0086 NEW) endif(POLICY CMP0086) -# Force C++ 14 -set(CMAKE_CXX_STANDARD 14) +# Force C++ 17 +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(BUILD_SHARED_LIBS ON) @@ -71,7 +71,7 @@ endmacro(option_with_default OPTION_NAME OPTION_STRING OPTION_DEFAULT) # OpenGL. If available, enable compilation for Visualization module # ##################################################################### find_package(OpenGL) -include_directories(OPENGL_INCLUDE_DIR) +include_directories(${OPENGL_INCLUDE_DIR}) ################# # Build options # @@ -196,7 +196,10 @@ if(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY) message(STATUS "conda-build running, using $ENV{SP_DIR} as install dir") set(PYTHONOCC_INSTALL_DIRECTORY $ENV{SP_DIR}/OCC CACHE PATH "pythonocc install directory") else(DEFINED ENV{SP_DIR} AND WIN32) - execute_process(COMMAND ${Python3_EXECUTABLE} -c "from distutils.sysconfig import get_python_lib; from os.path import relpath; print(relpath(get_python_lib(1,prefix='${CMAKE_INSTALL_PREFIX}'),'${CMAKE_INSTALL_PREFIX}'))" OUTPUT_VARIABLE python_lib OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process( + COMMAND ${Python3_EXECUTABLE} -c "import sysconfig, os, sys; print(os.path.relpath(sysconfig.get_path('platlib'), sys.prefix))" + OUTPUT_VARIABLE python_lib + OUTPUT_STRIP_TRAILING_WHITESPACE) set(PYTHONOCC_INSTALL_DIRECTORY ${python_lib}/OCC CACHE PATH "pythonocc install directory") endif(DEFINED ENV{SP_DIR} AND WIN32) endif(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY) @@ -292,7 +295,7 @@ foreach(OCCT_MODULE ${OCCT_TOOLKIT_MODEL}) set(FILE ${SWIG_FILES_PATH}/${OCCT_MODULE}.i) set_source_files_properties(${FILE} PROPERTIES CPLUSPLUS ON) swig_add_library (${OCCT_MODULE} LANGUAGE python SOURCES ${FILE} TYPE MODULE) - swig_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} Python3::Module) + target_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} Python3::Module) endforeach(OCCT_MODULE) ############### @@ -306,7 +309,7 @@ set(TESSELATOR_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/Tesselator/ShapeTesselator.cpp) swig_add_library(Tesselator LANGUAGE python SOURCES ${TESSELATOR_SOURCE_FILES} TYPE MODULE) -swig_link_libraries(Tesselator ${OCCT_MODEL_LIBRARIES} Python3::Module) +target_link_libraries(Tesselator ${OCCT_MODEL_LIBRARIES} Python3::Module) ################# # Visualisation # @@ -316,13 +319,13 @@ if(PYTHONOCC_WRAP_VISU) if(OPENGL_FOUND) message(STATUS "OpenGL found; Visualization support enabled") endif() - include_directories(OPENGL_INCLUDE_DIR) + include_directories(${OPENGL_INCLUDE_DIR}) foreach(OCCT_MODULE ${OCCT_TOOLKIT_VISUALIZATION}) set(FILE ${SWIG_FILES_PATH}/${OCCT_MODULE}.i) set_source_files_properties(${FILE} PROPERTIES CPLUSPLUS ON) swig_add_library (${OCCT_MODULE} LANGUAGE python SOURCES ${FILE} TYPE MODULE) - swig_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) + target_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) endforeach(OCCT_MODULE) # Build third part modules @@ -335,12 +338,12 @@ if(PYTHONOCC_WRAP_VISU) ${CMAKE_CURRENT_SOURCE_DIR}/src/Visualization/Display3d.cpp) swig_add_library(Visualization LANGUAGE python SOURCES ${VISUALIZATION_SOURCE_FILES} TYPE MODULE) - swig_link_libraries(Visualization ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) + target_link_libraries(Visualization ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) if(APPLE) # on OSX, always add /System/Library/Frameworks/Cocoa.framework, even # if GLX is enabled - swig_link_libraries(Visualization /System/Library/Frameworks/Cocoa.framework) + target_link_libraries(Visualization /System/Library/Frameworks/Cocoa.framework) endif(APPLE) ########################## @@ -355,7 +358,7 @@ if(PYTHONOCC_WRAP_VISU) ${CMAKE_CURRENT_SOURCE_DIR}/src/MeshDataSource/MeshDataSource.cpp) swig_add_library(MeshDS LANGUAGE python SOURCES ${MESHDATASOURCE_SOURCE_FILES} TYPE MODULE) - swig_link_libraries(MeshDS ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) + target_link_libraries(MeshDS ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) endif(PYTHONOCC_WRAP_VISU) @@ -367,7 +370,7 @@ if(PYTHONOCC_WRAP_DATAEXCHANGE) set(FILE ${SWIG_FILES_PATH}/${OCCT_MODULE}.i) set_source_files_properties(${FILE} PROPERTIES CPLUSPLUS ON) swig_add_library(${OCCT_MODULE} LANGUAGE python SOURCES ${FILE} TYPE MODULE) - swig_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} ${OCCT_DATAEXCHANGE_LIBRARIES} Python3::Module) + target_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} ${OCCT_DATAEXCHANGE_LIBRARIES} Python3::Module) endforeach(OCCT_MODULE) endif(PYTHONOCC_WRAP_DATAEXCHANGE) @@ -379,26 +382,28 @@ if(PYTHONOCC_WRAP_OCAF) set(FILE ${SWIG_FILES_PATH}/${OCCT_MODULE}.i) set_source_files_properties(${FILE} PROPERTIES CPLUSPLUS ON) swig_add_library(${OCCT_MODULE} LANGUAGE python SOURCES ${FILE} TYPE MODULE) - swig_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} ${OCCT_OCAF_LIBRARIES} Python3::Module) + target_link_libraries(${OCCT_MODULE} ${OCCT_MODEL_LIBRARIES} ${OCCT_OCAF_LIBRARIES} Python3::Module) endforeach(OCCT_MODULE) endif(PYTHONOCC_WRAP_OCAF) ########## # Addons # ########## -execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory src/Addons) -set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/Addons.i PROPERTIES CPLUSPLUS ON) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/Addons) -set(ADDONS_SOURCE_FILES -${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/Addons.i -${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/Font3d.cpp -#${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/TextItem.cpp -#${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/LineItem.cpp -#${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/TextureItem.cpp -) - -swig_add_library(Addons LANGUAGE python SOURCES ${ADDONS_SOURCE_FILES} TYPE MODULE) -swig_link_libraries(Addons ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) +if(PYTHONOCC_WRAP_VISU) + execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory src/Addons) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/Addons.i PROPERTIES CPLUSPLUS ON) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/Addons) + set(ADDONS_SOURCE_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/Addons.i + ${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/Font3d.cpp + #${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/TextItem.cpp + #${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/LineItem.cpp + #${CMAKE_CURRENT_SOURCE_DIR}/src/Addons/TextureItem.cpp + ) + + swig_add_library(Addons LANGUAGE python SOURCES ${ADDONS_SOURCE_FILES} TYPE MODULE) + target_link_libraries(Addons ${OCCT_MODEL_LIBRARIES} ${OCCT_VISUALIZATION_LIBRARIES} Python3::Module) +endif(PYTHONOCC_WRAP_VISU) ################ # Installation # @@ -461,8 +466,10 @@ if(PYTHONOCC_WRAP_VISU) endif(PYTHONOCC_WRAP_VISU) # install addons -install(FILES ${BUILD_DIR}/Addons.py DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}/Core ) -install(TARGETS Addons DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}/Core ) +if(PYTHONOCC_WRAP_VISU) + install(FILES ${BUILD_DIR}/Addons.py DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}/Core ) + install(TARGETS Addons DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}/Core ) +endif(PYTHONOCC_WRAP_VISU) # install Display install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Display DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 12541dc0d..5be7ad252 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -75,3 +75,45 @@ jobs: vmImage: 'windows-2022' py_maj: 3 py_min: 12 + +- template: conda-build.yml + parameters: + name: Ubuntu_24_04_python313 + vmImage: 'ubuntu-24.04' + py_maj: 3 + py_min: 13 + +- template: conda-build.yml + parameters: + name: macOS_12_python313 + vmImage: 'macOS-latest' + py_maj: 3 + py_min: 13 + +- template: conda-build.yml + parameters: + name: Windows_VS2022_python313 + vmImage: 'windows-2022' + py_maj: 3 + py_min: 13 + +- template: conda-build.yml + parameters: + name: Ubuntu_24_04_python314 + vmImage: 'ubuntu-24.04' + py_maj: 3 + py_min: 14 + +- template: conda-build.yml + parameters: + name: macOS_12_python314 + vmImage: 'macOS-latest' + py_maj: 3 + py_min: 14 + +- template: conda-build.yml + parameters: + name: Windows_VS2022_python314 + vmImage: 'windows-2022' + py_maj: 3 + py_min: 14 diff --git a/ci/conda/build.sh b/ci/conda/build.sh index 5a9b913dd..662ef3e55 100644 --- a/ci/conda/build.sh +++ b/ci/conda/build.sh @@ -1,7 +1,11 @@ #!/bin/bash -# make an in source build do to some problems with install # Configure step +EXTRA_CMAKE_ARGS="" +if [ "$(uname)" == "Darwin" ]; then + EXTRA_CMAKE_ARGS="-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13" +fi + cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=$PREFIX \ @@ -12,17 +16,11 @@ cmake -G Ninja \ -DPython3_FIND_STRATEGY=LOCATION \ -DPython3_FIND_FRAMEWORK=NEVER \ -DSWIG_HIDE_WARNINGS=ON \ - -DPYTHONOCC_MESHDS_NUMPY=ON + -DPYTHONOCC_MESHDS_NUMPY=ON \ + $EXTRA_CMAKE_ARGS # Build step ninja # Install step ninja install - -# fix rpaths -#if [ $(uname) == Darwin ]; then -# for lib in $(ls $SP_DIR/OCC/_*.so); do -# install_name_tool -rpath $PREFIX/lib @loader_path/../../../ $lib -# done -#fi diff --git a/ci/conda/meta.yaml b/ci/conda/meta.yaml index cb272cf1b..8777f1332 100644 --- a/ci/conda/meta.yaml +++ b/ci/conda/meta.yaml @@ -23,15 +23,20 @@ requirements: - ninja - cmake - swig ==4.4.1 + - sysroot_linux-64 >=2.28 # [linux] host: - python {{ python }} + - python_abi * *_cp313 [py==313] + - python_abi * *_cp314 [py==314] - occt ==7.9.3 - numpy >=1.17 run: - occt ==7.9.3 - numpy >=1.17 + - python_abi * *_cp313 [py==313] + - python_abi * *_cp314 [py==314] test: imports: @@ -39,12 +44,12 @@ test: - OCC.Core.BRepPrimAPI - OCC.Core.Tesselator requires: - - pyqt >=5 + - pyqt5-sip # [win] + - pyside6 >=6.10 # [win] + - wxpython >=4.2 # [win] + - svgwrite >=1.4 - mypy - pytest - - svgwrite - - wxpython >=4 - - pyside6 about: home: https://github.com/tpaviot/pythonocc-core diff --git a/conda-build.yml b/conda-build.yml index 309d09fd3..287a74ad8 100644 --- a/conda-build.yml +++ b/conda-build.yml @@ -44,7 +44,7 @@ jobs: conda info -a && \ conda config --add channels https://conda.anaconda.org/conda-forge displayName: 'Conda config and info' - - bash: conda create --yes --quiet --name build_env conda-build conda-verify libarchive python=${{ parameters.py_maj }}.${{ parameters.py_min }} anaconda-client + - bash: conda create --yes --quiet --name build_env conda-build conda-verify libarchive python=3.12 anaconda-client displayName: 'Create Anaconda environment' - ${{ if eq(parameters.vmImage, 'windows-2022') }}: - script: | @@ -54,7 +54,7 @@ jobs: set CC=cl.exe set CXX=cl.exe call activate build_env - conda-build --no-remove-work-dir --dirty ci/conda + conda-build --python ${{ parameters.py_maj }}.${{ parameters.py_min }} --no-remove-work-dir --dirty ci/conda displayName: 'Set Windows environment and build' env: CXX: "cl.exe" @@ -66,7 +66,7 @@ jobs: - ${{ if not(contains(parameters.vmImage, 'win')) }}: - bash: | source activate build_env && \ - conda-build --no-remove-work-dir --dirty ci/conda + conda-build --python ${{ parameters.py_maj }}.${{ parameters.py_min }} --no-remove-work-dir --dirty ci/conda displayName: 'Run conda build' failOnStderr: false env: From b1dbe1c19d5ffee71daaa44d52e0d311f6cd6563 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sat, 14 Feb 2026 18:25:32 +0100 Subject: [PATCH 08/12] Optimize ShapeTesselator speed and memory with C++17 idioms Replace std::ostringstream with std::to_chars + pre-reserved std::string in export functions (~37% faster). Use std::memcpy for bulk float copies in GetVertices/NormalsAsTuple (~23% faster) and JoinPrimitives. Skip identity transforms in ProcessSingleFace. Store Face/Edge by value instead of unique_ptr to eliminate per-face heap allocation. Remove redundant reserve() before resize() calls. Add [[nodiscard]] on getters. Remove unused includes (memory, tuple, sstream, iomanip, numeric, mutex). --- src/Tesselator/ShapeTesselator.cpp | 401 +++++++++++++++-------------- src/Tesselator/ShapeTesselator.h | 52 ++-- 2 files changed, 229 insertions(+), 224 deletions(-) diff --git a/src/Tesselator/ShapeTesselator.cpp b/src/Tesselator/ShapeTesselator.cpp index 1a42afd54..51c6afdad 100644 --- a/src/Tesselator/ShapeTesselator.cpp +++ b/src/Tesselator/ShapeTesselator.cpp @@ -17,22 +17,21 @@ #include "ShapeTesselator.h" -#include #include +#include #include -#include -#include -#include -#include +#include #include #include -#include +#include +#include +#include #ifdef _OPENMP #include #endif -// OpenCASCADE includes +// OpenCASCADE includes #include #include #include @@ -55,23 +54,32 @@ #include // ======================================================================== -// Face structure implementation +// Fast float-to-string helpers using C++17 std::to_chars // ======================================================================== -void ShapeTesselator::Face::reserve(size_t vertices, size_t triangles) { - vertex_coords.reserve(vertices * 3); - normal_coords.reserve(vertices * 3); - triangle_indices.reserve(triangles * 3); +namespace { + //! Append a float to a string using std::to_chars (no locale, no virtual dispatch) + inline void appendFloat(std::string& out, float f) { + char buf[32]; + auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), f); + out.append(buf, static_cast(ptr - buf)); + } + + //! Append a float with epsilon clamping (for X3D export compatibility) + inline void appendFloatWithEpsilon(std::string& out, float f) { + constexpr float epsilon = 1e-3f; + if (std::abs(f) < epsilon) { + out.push_back('0'); + } else { + appendFloat(out, f); + } + } } // ======================================================================== -// Edge structure implementation +// Edge structure implementation // ======================================================================== -void ShapeTesselator::Edge::reserve(size_t vertices) { - vertex_coords.reserve(vertices * 3); -} - Standard_Integer ShapeTesselator::Edge::size() const noexcept { return static_cast(vertex_coords.size() / 3); } @@ -83,10 +91,6 @@ Standard_Integer ShapeTesselator::Edge::size() const noexcept { ShapeTesselator::ShapeTesselator(const TopoDS_Shape& aShape) : computed(false), use_parallel(false), myShape(aShape) { ComputeDefaultDeviation(); - - // Reserve memory for face and edge collections based on estimates - face_list.reserve(100); // Initial estimate - edge_list.reserve(200); // Initial estimate } void ShapeTesselator::Compute(bool compute_edges, float mesh_quality, bool parallel) { @@ -114,7 +118,7 @@ void ShapeTesselator::ComputeDefaultDeviation() { } aBox.Get(aXmin, aYmin, aZmin, aXmax, aYmax, aZmax); - + const auto max_dimension = std::max({aXmax - aXmin, aYmax - aYmin, aZmax - aZmin}); myDeviation = max_dimension * 2e-2; } @@ -154,7 +158,7 @@ void ShapeTesselator::ProcessFaces(const std::vector& faces) { #ifdef _OPENMP if (use_parallel && num_faces > 1) { // Parallel processing: create face data in parallel, then collect results - std::vector> local_results(num_faces); + std::vector local_results(num_faces); #pragma omp parallel for schedule(dynamic) for (Standard_Integer i = 0; i < num_faces; ++i) { @@ -166,17 +170,17 @@ void ShapeTesselator::ProcessFaces(const std::vector& faces) { continue; } - auto face_data = std::make_unique(); - ProcessSingleFace(face, triangulation, location, *face_data); + Face face_data; + ProcessSingleFace(face, triangulation, location, face_data); - if (face_data->number_of_triangles > 0) { + if (face_data.number_of_triangles > 0) { local_results[i] = std::move(face_data); } } - // Collect non-null results + // Collect non-empty results for (auto& result : local_results) { - if (result) { + if (result.number_of_triangles > 0) { face_list.push_back(std::move(result)); } } @@ -192,35 +196,45 @@ void ShapeTesselator::ProcessFaces(const std::vector& faces) { continue; } - auto face_data = std::make_unique(); - ProcessSingleFace(face, triangulation, location, *face_data); + Face face_data; + ProcessSingleFace(face, triangulation, location, face_data); - if (face_data->number_of_triangles > 0) { + if (face_data.number_of_triangles > 0) { face_list.push_back(std::move(face_data)); } } } } -void ShapeTesselator::ProcessSingleFace(const TopoDS_Face& face, +void ShapeTesselator::ProcessSingleFace(const TopoDS_Face& face, const Handle(Poly_Triangulation)& triangulation, const TopLoc_Location& location, Face& face_data) { - + const auto nb_nodes = triangulation->NbNodes(); const auto nb_triangles = triangulation->NbTriangles(); - - // Pre-allocate - face_data.reserve(nb_nodes, nb_triangles); - - // Process vertices with transformation in single pass + + // Process vertices - skip transform when location is identity face_data.vertex_coords.resize(nb_nodes * 3); - for (Standard_Integer i = 1; i <= nb_nodes; ++i) { - const auto point = triangulation->Node(i).Transformed(location).XYZ(); - const auto idx = (i - 1) * 3; - face_data.vertex_coords[idx] = static_cast(point.X()); - face_data.vertex_coords[idx + 1] = static_cast(point.Y()); - face_data.vertex_coords[idx + 2] = static_cast(point.Z()); + + if (location.IsIdentity()) { + for (Standard_Integer i = 1; i <= nb_nodes; ++i) { + const auto& point = triangulation->Node(i); + const auto idx = (i - 1) * 3; + face_data.vertex_coords[idx] = static_cast(point.X()); + face_data.vertex_coords[idx + 1] = static_cast(point.Y()); + face_data.vertex_coords[idx + 2] = static_cast(point.Z()); + } + } else { + const auto& trsf = location.Transformation(); + for (Standard_Integer i = 1; i <= nb_nodes; ++i) { + auto point = triangulation->Node(i); + point.Transform(trsf); + const auto idx = (i - 1) * 3; + face_data.vertex_coords[idx] = static_cast(point.X()); + face_data.vertex_coords[idx + 1] = static_cast(point.Y()); + face_data.vertex_coords[idx + 2] = static_cast(point.Z()); + } } // Process normals - prefer pre-computed normals, fallback to UV computation @@ -300,7 +314,6 @@ void ShapeTesselator::ProcessTriangles(const TopoDS_Face& face, const auto nb_triangles = triangulation->NbTriangles(); const auto is_reversed = (face.Orientation() == TopAbs_REVERSED); - // Pre-allocate exact size face_data.triangle_indices.resize(nb_triangles * 3); face_data.number_of_triangles = nb_triangles; @@ -320,46 +333,60 @@ void ShapeTesselator::ProcessTriangles(const TopoDS_Face& face, } void ShapeTesselator::JoinPrimitives() { - // Calculate totals in single pass using std::accumulate - auto totals = std::accumulate(face_list.begin(), face_list.end(), - std::tuple{}, - [](const auto& acc, const auto& face) { - return std::make_tuple( - std::get<0>(acc) + face->number_of_triangles, - std::get<1>(acc) + face->number_of_invalid_triangles, - std::get<2>(acc) + static_cast(face->vertex_coords.size() / 3), - std::get<3>(acc) + static_cast(face->normal_coords.size() / 3), - std::get<4>(acc) + face->number_of_invalid_normals - ); - }); - - std::tie(tot_triangle_count, tot_invalid_triangle_count, - tot_vertex_count, tot_normal_count, tot_invalid_normal_count) = totals; - - // Single allocation of consolidated arrays - consolidated_vertices.reserve(tot_vertex_count * 3); - consolidated_normals.reserve(tot_normal_count * 3); - consolidated_triangle_indices.reserve(tot_triangle_count * 3); - - // Consolidation with move semantics - Standard_Integer vertex_offset = 0; + // Calculate totals in a single pass + tot_triangle_count = 0; + tot_invalid_triangle_count = 0; + tot_vertex_count = 0; + tot_normal_count = 0; + tot_invalid_normal_count = 0; + + for (const auto& face : face_list) { + tot_triangle_count += face.number_of_triangles; + tot_invalid_triangle_count += face.number_of_invalid_triangles; + tot_vertex_count += static_cast(face.vertex_coords.size() / 3); + tot_normal_count += static_cast(face.normal_coords.size() / 3); + tot_invalid_normal_count += face.number_of_invalid_normals; + } + + // Single allocation of consolidated arrays (resize, not reserve, for memcpy) + consolidated_vertices.resize(tot_vertex_count * 3); + consolidated_normals.resize(tot_normal_count * 3); + consolidated_triangle_indices.resize(tot_triangle_count * 3); + + // Consolidate using memcpy for contiguous POD data + size_t vertex_offset = 0; + size_t normal_offset = 0; + size_t tri_offset = 0; + Standard_Integer index_offset = 0; + for (auto& face : face_list) { - // Move vertex coordinates - consolidated_vertices.insert(consolidated_vertices.end(), - std::make_move_iterator(face->vertex_coords.begin()), - std::make_move_iterator(face->vertex_coords.end())); - - // Move normals - consolidated_normals.insert(consolidated_normals.end(), - std::make_move_iterator(face->normal_coords.begin()), - std::make_move_iterator(face->normal_coords.end())); - - // Adjust and insert triangle indices - for (auto& index : face->triangle_indices) { - consolidated_triangle_indices.push_back(index + vertex_offset - 1); + // Bulk-copy vertex coordinates + const auto vert_count = face.vertex_coords.size(); + if (vert_count > 0) { + std::memcpy(consolidated_vertices.data() + vertex_offset, + face.vertex_coords.data(), + vert_count * sizeof(float)); } - vertex_offset += static_cast(face->vertex_coords.size() / 3); + // Bulk-copy normal coordinates + const auto norm_count = face.normal_coords.size(); + if (norm_count > 0) { + std::memcpy(consolidated_normals.data() + normal_offset, + face.normal_coords.data(), + norm_count * sizeof(float)); + } + + // Adjust triangle indices (1-based per-face → 0-based global) + const auto tri_count = face.triangle_indices.size(); + for (size_t k = 0; k < tri_count; ++k) { + consolidated_triangle_indices[tri_offset + k] = + face.triangle_indices[k] + index_offset - 1; + } + + vertex_offset += vert_count; + normal_offset += norm_count; + tri_offset += tri_count; + index_offset += static_cast(vert_count / 3); } // Release memory from individual faces @@ -369,26 +396,26 @@ void ShapeTesselator::JoinPrimitives() { void ShapeTesselator::ComputeEdges() { edge_list.clear(); - + TopTools_IndexedMapOfShape edge_map; TopExp::MapShapes(myShape, TopAbs_EDGE, edge_map); - + TopTools_IndexedDataMapOfShapeListOfShape edge_face_map; TopExp::MapShapesAndAncestors(myShape, TopAbs_EDGE, TopAbs_FACE, edge_face_map); - + edge_list.reserve(edge_map.Extent()); - + for (Standard_Integer i = 1; i <= edge_face_map.Extent(); ++i) { const auto& face_list_for_edge = edge_face_map.FindFromIndex(i); - + if (face_list_for_edge.IsEmpty()) { continue; // Skip free edges } - + const auto& edge = TopoDS::Edge(edge_map(i)); - auto edge_data = std::make_unique(); - - if (ProcessSingleEdge(edge, edge_face_map, i, *edge_data)) { + Edge edge_data; + + if (ProcessSingleEdge(edge, edge_face_map, i, edge_data)) { edge_list.push_back(std::move(edge_data)); } } @@ -398,13 +425,13 @@ bool ShapeTesselator::ProcessSingleEdge(const TopoDS_Edge& edge, const TopTools_IndexedDataMapOfShapeListOfShape& edge_face_map, Standard_Integer edge_index, Edge& edge_data) { - + TopLoc_Location location; gp_Trsf transform; - + // Try direct 3D triangulation first auto poly_3d = BRep_Tool::Polygon3D(edge, location); - + if (!poly_3d.IsNull()) { if (!location.IsIdentity()) { transform = location.Transformation(); @@ -413,7 +440,6 @@ bool ShapeTesselator::ProcessSingleEdge(const TopoDS_Edge& edge, const auto& nodes = poly_3d->Nodes(); const auto nb_nodes = poly_3d->NbNodes(); - // Pre-allocate exact size edge_data.vertex_coords.resize(nb_nodes * 3); for (Standard_Integer i = 1; i <= nb_nodes; ++i) { @@ -427,20 +453,20 @@ bool ShapeTesselator::ProcessSingleEdge(const TopoDS_Edge& edge, } return true; } - + // Fallback to face triangulation const auto& first_face = TopoDS::Face(edge_face_map.FindFromIndex(edge_index).First()); auto face_triangulation = BRep_Tool::Triangulation(first_face, location); - + if (face_triangulation.IsNull()) { return false; } - + auto poly_on_tri = BRep_Tool::PolygonOnTriangulation(edge, face_triangulation, location); if (poly_on_tri.IsNull()) { return false; } - + if (!location.IsIdentity()) { transform = location.Transformation(); } @@ -448,7 +474,6 @@ bool ShapeTesselator::ProcessSingleEdge(const TopoDS_Edge& edge, const auto& indices = poly_on_tri->Nodes(); const auto nb_nodes = poly_on_tri->NbNodes(); - // Pre-allocate exact size edge_data.vertex_coords.resize(nb_nodes * 3); for (Standard_Integer i = 1; i <= nb_nodes; ++i) { @@ -505,7 +530,7 @@ Standard_Integer ShapeTesselator::ObjEdgeGetVertexCount(Standard_Integer iEdge) if (iEdge < 0 || iEdge >= static_cast(edge_list.size())) { return 0; } - return edge_list[iEdge]->size(); + return edge_list[iEdge].size(); } const float* ShapeTesselator::VerticesList() const { @@ -522,14 +547,13 @@ std::vector ShapeTesselator::GetVerticesPositionAsTuple() const { const auto total_floats = tot_triangle_count * 9; // 3 vertices * 3 coords std::vector result(total_floats); - Standard_Integer out_idx = 0; + float* out = result.data(); for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { const auto base_idx = i * 3; for (int j = 0; j < 3; ++j) { const auto vertex_idx = consolidated_triangle_indices[base_idx + j] * 3; - result[out_idx++] = consolidated_vertices[vertex_idx]; - result[out_idx++] = consolidated_vertices[vertex_idx + 1]; - result[out_idx++] = consolidated_vertices[vertex_idx + 2]; + std::memcpy(out, &consolidated_vertices[vertex_idx], 3 * sizeof(float)); + out += 3; } } @@ -542,14 +566,13 @@ std::vector ShapeTesselator::GetNormalsAsTuple() const { const auto total_floats = tot_triangle_count * 9; std::vector result(total_floats); - Standard_Integer out_idx = 0; + float* out = result.data(); for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { const auto base_idx = i * 3; for (int j = 0; j < 3; ++j) { const auto normal_idx = consolidated_triangle_indices[base_idx + j] * 3; - result[out_idx++] = consolidated_normals[normal_idx]; - result[out_idx++] = consolidated_normals[normal_idx + 1]; - result[out_idx++] = consolidated_normals[normal_idx + 2]; + std::memcpy(out, &consolidated_normals[normal_idx], 3 * sizeof(float)); + out += 3; } } @@ -560,7 +583,7 @@ void ShapeTesselator::GetVertex(Standard_Integer index, float& x, float& y, floa if (!computed || index < 0 || index >= tot_vertex_count) { throw std::out_of_range("Vertex index out of range"); } - + const auto base_idx = index * 3; x = consolidated_vertices[base_idx]; y = consolidated_vertices[base_idx + 1]; @@ -571,47 +594,47 @@ void ShapeTesselator::GetNormal(Standard_Integer index, float& x, float& y, floa if (!computed || index < 0 || index >= tot_normal_count) { throw std::out_of_range("Normal index out of range"); } - + const auto base_idx = index * 3; x = consolidated_normals[base_idx]; y = consolidated_normals[base_idx + 1]; z = consolidated_normals[base_idx + 2]; } -void ShapeTesselator::GetTriangleIndex(Standard_Integer triangle_idx, +void ShapeTesselator::GetTriangleIndex(Standard_Integer triangle_idx, Standard_Integer& v1, Standard_Integer& v2, Standard_Integer& v3) const { if (!computed || triangle_idx < 0 || triangle_idx >= tot_triangle_count) { throw std::out_of_range("Triangle index out of range"); } - + const auto base_idx = triangle_idx * 3; v1 = consolidated_triangle_indices[base_idx]; v2 = consolidated_triangle_indices[base_idx + 1]; v3 = consolidated_triangle_indices[base_idx + 2]; } -void ShapeTesselator::GetEdgeVertex(Standard_Integer iEdge, Standard_Integer ivert, +void ShapeTesselator::GetEdgeVertex(Standard_Integer iEdge, Standard_Integer ivert, float& x, float& y, float& z) const { if (!computed || iEdge < 0 || iEdge >= static_cast(edge_list.size())) { throw std::out_of_range("Edge index out of range"); } - + const auto& edge = edge_list[iEdge]; - if (ivert < 0 || ivert >= edge->size()) { + if (ivert < 0 || ivert >= edge.size()) { throw std::out_of_range("Edge vertex index out of range"); } - + const auto base_idx = ivert * 3; - x = edge->vertex_coords[base_idx]; - y = edge->vertex_coords[base_idx + 1]; - z = edge->vertex_coords[base_idx + 2]; + x = edge.vertex_coords[base_idx]; + y = edge.vertex_coords[base_idx + 1]; + z = edge.vertex_coords[base_idx + 2]; } void ShapeTesselator::ObjGetTriangle(Standard_Integer trianglenum, Standard_Integer* vertices, Standard_Integer* normals) const { if (!computed || trianglenum < 0 || trianglenum >= tot_triangle_count) { return; } - + const auto base_idx = trianglenum * 3; const auto pID = consolidated_triangle_indices[base_idx] * 3; const auto qID = consolidated_triangle_indices[base_idx + 1] * 3; @@ -630,118 +653,112 @@ void ShapeTesselator::ObjGetTriangle(Standard_Integer trianglenum, Standard_Inte // Export functionality // ======================================================================== -namespace { - //! Format float number with epsilon handling directly to stream - inline void writeFloat(std::ostringstream& out, float f) { - constexpr float epsilon = 1e-3f; - if (std::abs(f) < epsilon) { - out << "0"; - } else { - out << f; - } - } -} - std::string ShapeTesselator::ExportShapeToThreejsJSONString(const char* shape_function_name) const { if (!computed) return "{}"; - - std::ostringstream json; - json << std::fixed << std::setprecision(6); - - json << "{\n" - << "\t\"metadata\": {\n" - << "\t\t\"version\": 4.4,\n" - << "\t\t\"type\": \"BufferGeometry\",\n" - << "\t\t\"generator\": \"pythonOCC-optimized\"\n" - << "\t},\n" - << "\t\"uuid\": \"" << shape_function_name << "\",\n" - << "\t\"type\": \"BufferGeometry\",\n" - << "\t\"data\": {\n" - << "\t\t\"attributes\": {\n" - << "\t\t\t\"position\": {\n" - << "\t\t\t\t\"itemSize\": 3,\n" - << "\t\t\t\t\"type\": \"Float32Array\",\n" - << "\t\t\t\t\"array\": ["; - - // Export vertices efficiently without creating intermediate vector + + // Pre-allocate: ~15 chars per float, 9 floats per triangle, x2 for verts+normals + const size_t estimated_size = 512 + static_cast(tot_triangle_count) * 9 * 15 * 2; + + std::string json; + json.reserve(estimated_size); + + json.append("{\n\t\"metadata\": {\n\t\t\"version\": 4.4,\n\t\t\"type\": \"BufferGeometry\",\n" + "\t\t\"generator\": \"pythonOCC-optimized\"\n\t},\n\t\"uuid\": \""); + json.append(shape_function_name); + json.append("\",\n\t\"type\": \"BufferGeometry\",\n\t\"data\": {\n\t\t\"attributes\": {\n" + "\t\t\t\"position\": {\n\t\t\t\t\"itemSize\": 3,\n\t\t\t\t\"type\": \"Float32Array\",\n" + "\t\t\t\t\"array\": ["); + + // Export vertices using fast to_chars for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { const auto base_idx = i * 3; for (int j = 0; j < 3; ++j) { - if (i > 0 || j > 0) json << ","; + if (i > 0 || j > 0) json.push_back(','); const auto vertex_idx = consolidated_triangle_indices[base_idx + j] * 3; - json << consolidated_vertices[vertex_idx] << "," - << consolidated_vertices[vertex_idx + 1] << "," - << consolidated_vertices[vertex_idx + 2]; + appendFloat(json, consolidated_vertices[vertex_idx]); + json.push_back(','); + appendFloat(json, consolidated_vertices[vertex_idx + 1]); + json.push_back(','); + appendFloat(json, consolidated_vertices[vertex_idx + 2]); } } - json << "]\n\t\t\t},\n" - << "\t\t\t\"normal\": {\n" - << "\t\t\t\t\"itemSize\": 3,\n" - << "\t\t\t\t\"type\": \"Float32Array\",\n" - << "\t\t\t\t\"array\": ["; + json.append("]\n\t\t\t},\n\t\t\t\"normal\": {\n\t\t\t\t\"itemSize\": 3,\n" + "\t\t\t\t\"type\": \"Float32Array\",\n\t\t\t\t\"array\": ["); - // Export normals efficiently without creating intermediate vector + // Export normals using fast to_chars for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { const auto base_idx = i * 3; for (int j = 0; j < 3; ++j) { - if (i > 0 || j > 0) json << ","; + if (i > 0 || j > 0) json.push_back(','); const auto normal_idx = consolidated_triangle_indices[base_idx + j] * 3; - json << consolidated_normals[normal_idx] << "," - << consolidated_normals[normal_idx + 1] << "," - << consolidated_normals[normal_idx + 2]; + appendFloat(json, consolidated_normals[normal_idx]); + json.push_back(','); + appendFloat(json, consolidated_normals[normal_idx + 1]); + json.push_back(','); + appendFloat(json, consolidated_normals[normal_idx + 2]); } } - json << "]\n\t\t\t}\n" - << "\t\t}\n" - << "\t}\n" - << "}"; + json.append("]\n\t\t\t}\n\t\t}\n\t}\n}"); - return json.str(); + return json; } std::string ShapeTesselator::ExportShapeToX3DTriangleSet() const { if (!computed) return ""; - std::ostringstream str_vertices, str_normals; + // Pre-allocate: ~12 chars per float, 9 floats per triangle, x2 for verts+normals + const size_t estimated_floats = static_cast(tot_triangle_count) * 9; + const size_t estimated_per_string = estimated_floats * 12; + + std::string str_vertices, str_normals; + str_vertices.reserve(estimated_per_string); + str_normals.reserve(estimated_per_string); - // Process triangles and build vertex/normal strings directly for (Standard_Integer i = 0; i < tot_triangle_count; ++i) { const auto base_idx = i * 3; - // Process all 3 vertices of the triangle for (int j = 0; j < 3; ++j) { const auto vertex_idx = consolidated_triangle_indices[base_idx + j] * 3; - writeFloat(str_vertices, consolidated_vertices[vertex_idx]); str_vertices << ' '; - writeFloat(str_vertices, consolidated_vertices[vertex_idx + 1]); str_vertices << ' '; - writeFloat(str_vertices, consolidated_vertices[vertex_idx + 2]); str_vertices << ' '; - writeFloat(str_normals, consolidated_normals[vertex_idx]); str_normals << ' '; - writeFloat(str_normals, consolidated_normals[vertex_idx + 1]); str_normals << ' '; - writeFloat(str_normals, consolidated_normals[vertex_idx + 2]); str_normals << ' '; + appendFloatWithEpsilon(str_vertices, consolidated_vertices[vertex_idx]); + str_vertices.push_back(' '); + appendFloatWithEpsilon(str_vertices, consolidated_vertices[vertex_idx + 1]); + str_vertices.push_back(' '); + appendFloatWithEpsilon(str_vertices, consolidated_vertices[vertex_idx + 2]); + str_vertices.push_back(' '); + + appendFloatWithEpsilon(str_normals, consolidated_normals[vertex_idx]); + str_normals.push_back(' '); + appendFloatWithEpsilon(str_normals, consolidated_normals[vertex_idx + 1]); + str_normals.push_back(' '); + appendFloatWithEpsilon(str_normals, consolidated_normals[vertex_idx + 2]); + str_normals.push_back(' '); } } - std::ostringstream result; - result << "\n"; - result << "\n"; - result << "\n"; - result << "\n"; + std::string result; + result.reserve(str_vertices.size() + str_normals.size() + 128); + result.append("\n\n\n\n"); - return result.str(); + return result; } void ShapeTesselator::ExportShapeToX3D(const char* filename, int diffR, int diffG, int diffB) { EnsureMeshIsComputed(); - + std::ofstream x3d_file(filename); if (!x3d_file.is_open()) { throw std::runtime_error("Cannot open file for writing"); } - + // Write X3D header x3d_file << ""; x3d_file << ""; @@ -750,15 +767,15 @@ void ShapeTesselator::ExportShapeToX3D(const char* filename, int diffR, int diff x3d_file << ""; x3d_file << ""; x3d_file << "(diffR) / 255.0f; const auto g = static_cast(diffG) / 255.0f; const auto b = static_cast(diffB) / 255.0f; - + x3d_file << "diffuseColor='" << r << " " << g << " " << b << "' "; x3d_file << "specularColor='0.2 0.2 0.2'>"; - + // Write tessellation x3d_file << ExportShapeToX3DTriangleSet(); x3d_file << "\n"; diff --git a/src/Tesselator/ShapeTesselator.h b/src/Tesselator/ShapeTesselator.h index b171fca4e..174204308 100644 --- a/src/Tesselator/ShapeTesselator.h +++ b/src/Tesselator/ShapeTesselator.h @@ -21,9 +21,7 @@ #pragma once #include -#include #include -#include // OpenCASCADE includes #include @@ -34,7 +32,6 @@ #include #include #include -#include class ShapeTesselator { public: @@ -46,23 +43,14 @@ class ShapeTesselator { Standard_Integer number_of_invalid_triangles = 0; //!< Number of invalid triangles Standard_Integer number_of_invalid_normals = 0; //!< Number of invalid normals Standard_Integer number_of_normals = 0; //!< Number of normal vectors - - //! Reserve memory to avoid reallocations - //! @param vertices Expected number of vertices - //! @param triangles Expected number of triangles - void reserve(size_t vertices, size_t triangles); }; struct Edge { std::vector vertex_coords; //!< Edge vertex coordinates - - //! Reserve memory for vertices - //! @param vertices Expected number of vertices - void reserve(size_t vertices); - + //! Get number of vertices in this edge //! @return Number of vertices - Standard_Integer size() const noexcept; + [[nodiscard]] Standard_Integer size() const noexcept; }; private: @@ -72,9 +60,9 @@ class ShapeTesselator { TopoDS_Shape myShape; //!< The shape to tessellate Standard_Real myDeviation; //!< Tessellation deviation parameter - // Face and edge collections using smart pointers - std::vector> face_list; //!< Collection of tessellated faces - std::vector> edge_list; //!< Collection of tessellated edges + // Face and edge collections stored by value (no heap indirection) + std::vector face_list; //!< Collection of tessellated faces + std::vector edge_list; //!< Collection of tessellated edges // Consolidated mesh data for efficient access std::vector consolidated_vertices; //!< All vertex coordinates @@ -119,35 +107,35 @@ class ShapeTesselator { //! Get the current deviation parameter //! @return The deviation value - Standard_Real GetDeviation() const noexcept; + [[nodiscard]] Standard_Real GetDeviation() const noexcept; //! Ensure that mesh computation has been performed void EnsureMeshIsComputed(); // Mesh statistics getters - Standard_Integer ObjGetTriangleCount() const noexcept; //!< Get total triangle count - Standard_Integer ObjGetVertexCount() const noexcept; //!< Get total vertex count - Standard_Integer ObjGetNormalCount() const noexcept; //!< Get total normal count - Standard_Integer ObjGetInvalidTriangleCount() const noexcept; //!< Get invalid triangle count - Standard_Integer ObjGetInvalidNormalCount() const noexcept; //!< Get invalid normal count - Standard_Integer ObjGetEdgeCount() const noexcept; //!< Get edge count + [[nodiscard]] Standard_Integer ObjGetTriangleCount() const noexcept; + [[nodiscard]] Standard_Integer ObjGetVertexCount() const noexcept; + [[nodiscard]] Standard_Integer ObjGetNormalCount() const noexcept; + [[nodiscard]] Standard_Integer ObjGetInvalidTriangleCount() const noexcept; + [[nodiscard]] Standard_Integer ObjGetInvalidNormalCount() const noexcept; + [[nodiscard]] Standard_Integer ObjGetEdgeCount() const noexcept; //! Get number of vertices in a specific edge //! @param iEdge Edge index //! @return Number of vertices in the edge - Standard_Integer ObjEdgeGetVertexCount(Standard_Integer iEdge) const; + [[nodiscard]] Standard_Integer ObjEdgeGetVertexCount(Standard_Integer iEdge) const; // Direct data access - const float* VerticesList() const; //!< Get pointer to vertex data - const float* NormalsList() const; //!< Get pointer to normal data + [[nodiscard]] const float* VerticesList() const; + [[nodiscard]] const float* NormalsList() const; //! Get vertices as a flat array suitable for rendering //! @return Vector of vertex positions for all triangles - std::vector GetVerticesPositionAsTuple() const; + [[nodiscard]] std::vector GetVerticesPositionAsTuple() const; - //! Get normals as a flat array suitable for rendering + //! Get normals as a flat array suitable for rendering //! @return Vector of normal vectors for all triangles - std::vector GetNormalsAsTuple() const; + [[nodiscard]] std::vector GetNormalsAsTuple() const; //! Get vertex coordinates by index //! @param index Vertex index @@ -175,11 +163,11 @@ class ShapeTesselator { //! Export shape as Three.js JSON BufferGeometry //! @param shape_function_name Name/UUID for the geometry //! @return JSON string representation - std::string ExportShapeToThreejsJSONString(const char* shape_function_name) const; + [[nodiscard]] std::string ExportShapeToThreejsJSONString(const char* shape_function_name) const; //! Export shape as X3D TriangleSet //! @return X3D string representation - std::string ExportShapeToX3DTriangleSet() const; + [[nodiscard]] std::string ExportShapeToX3DTriangleSet() const; //! Export complete X3D file //! @param filename Output filename From a6e0d97e08a112a48599e01771679923eaec2a2b Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 15 Feb 2026 07:16:14 +0100 Subject: [PATCH 09/12] Fix macOS build: replace std::to_chars(float) with snprintf std::to_chars for floating point is unavailable on macOS with older SDKs (requires macOS 13.3+), breaking conda-forge builds. --- src/Tesselator/ShapeTesselator.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tesselator/ShapeTesselator.cpp b/src/Tesselator/ShapeTesselator.cpp index 51c6afdad..754757260 100644 --- a/src/Tesselator/ShapeTesselator.cpp +++ b/src/Tesselator/ShapeTesselator.cpp @@ -18,8 +18,8 @@ #include "ShapeTesselator.h" #include -#include #include +#include #include #include #include @@ -54,15 +54,15 @@ #include // ======================================================================== -// Fast float-to-string helpers using C++17 std::to_chars +// Fast float-to-string helpers // ======================================================================== namespace { - //! Append a float to a string using std::to_chars (no locale, no virtual dispatch) + //! Append a float to a string using snprintf (portable across all platforms) inline void appendFloat(std::string& out, float f) { char buf[32]; - auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), f); - out.append(buf, static_cast(ptr - buf)); + int len = std::snprintf(buf, sizeof(buf), "%g", f); + out.append(buf, static_cast(len)); } //! Append a float with epsilon clamping (for X3D export compatibility) From 779838f352694711d60c1567ed3f6e06f3826aa2 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Mon, 16 Feb 2026 06:36:52 +0100 Subject: [PATCH 10/12] Prepare release 7.9.3 --- CMakeLists.txt | 2 +- NEWS | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 12 +++++----- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ecc740191..678dfba00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ set(PYTHONOCC_VERSION_MINOR 9) set(PYTHONOCC_VERSION_PATCH 3) # Empty for official releases, set to -dev, -rc1, etc for development releases -set(PYTHONOCC_VERSION_DEVEL -dev) +set(PYTHONOCC_VERSION_DEVEL ) # set OCCT version set(OCCT_VERSION_MAJOR 7) diff --git a/NEWS b/NEWS index 2abe5b12f..580cc81d5 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,67 @@ +Version 7.9.3 - February 2026 +============================= + +This release requires opencascade-7.9.3 + +Highlights: +- OpenCASCADE 7.9.3 and SWIG 4.4.1 +- C++17 standard +- Python 3.13 and 3.14 support (Python 3.9 dropped) +- Tesselator performance: up to 37% faster exports, 50% less memory +- OpenGL Core Profile API for Qt6 integration +- Several memory leak fixes in SWIG typemaps and handle management +- Type stubs (.pyi) for Display and Extend packages + +* wrapper: upgrade to OpenCASCADE 7.9.3, bump SWIG to 4.4.1 + +* wrapper: bump C++ standard to C++17 + +* wrapper: add support for Python 3.13 and 3.14, drop Python 3.9 + +* wrapper: fix memory leak in TopoDS_Shape output typemaps + +* wrapper: fix memory leaks in SWIG wrappers (FunctionTransformers, OccHandle) + +* wrapper: add template for non-const byref handles, #1443 + +* wrapper: use Delete() method for occ handles + +* wrapper: use builtin SWIG_Python_AppendOutput + +* wrapper: refactored IOStream and exception catcher + +* wrapper: wrap NCollection_List iterator + +* wrapper: improve wrapper for NCollection_Sequence, __iter__ method added + +* wrapper: add docstrings and stubs (.pyi) for Display and Extend packages + +* wrapper: fix type hints for Display and Extend packages + +* wrapper: overall blackification (code formatting) + +* tesselator: optimize ShapeTesselator speed and memory with C++17 idioms + +* tesselator: switch from double to float for mesh vertices and normals, halving memory usage + +* display: add OpenGL Core Profile API to Display3d + +* display: add SetSRGBDisabled() to disable sRGB framebuffer + +* display: extend selection mode with wires and shells + +* display: don't draw seam edges in OCCViewer, #1413 + +* data exchange: add sew and make_solid options to STL importer for solid construction from 2d triangular mesh + +* build: add DEBUG_MEMORY compilation mode + +* build: fix macOS build (std::to_chars float compatibility) + +* build: fix conda build issues (sysroot, OpenGL path, dylib warnings) + +* ci/cd: use Windows 2022 in Azure pipelines + Version 7.9.0 - April 2025 ========================== diff --git a/README.md b/README.md index f5e042987..d0b13b141 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Azure Build Status](https://dev.azure.com/tpaviot/pythonocc-core/_apis/build/status/tpaviot.pythonocc-core?branchName=master)](https://dev.azure.com/tpaviot/pythonocc-core/_build?definitionId=2) [![Downloads Badge](https://anaconda.org/conda-forge/pythonocc-core/badges/downloads.svg)](https://anaconda.org/conda-forge/pythonocc-core) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/67c121324b8d4f37bc27029464c87020)](https://www.codacy.com/app/tpaviot/pythonocc-core?utm_source=github.com&utm_medium=referral&utm_content=tpaviot/pythonocc-core&utm_campaign=Badge_Grade) -[![Binder](http://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/tpaviot/pythonocc-binderhub/7.9.0) +[![Binder](http://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/tpaviot/pythonocc-binderhub/7.9.3) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3605364.svg)](https://doi.org/10.5281/zenodo.3605364) pythonocc-core @@ -12,7 +12,7 @@ About ----- pythonocc provides 3D modeling and dataexchange features. It is intended for CAD/PDM/PLM/BIM development. It is based on the OpenCascade Technology modeling kernel. -Latest release: [pythonocc-core 7.9.0 (April 2025)](https://github.com/tpaviot/pythonocc-core/releases/tag/7.9.0) +Latest release: [pythonocc-core 7.9.3 (February 2026)](https://github.com/tpaviot/pythonocc-core/releases/tag/7.9.3) Features -------- @@ -27,17 +27,17 @@ pythonocc provides the following features: Try online at mybinder ---------------------- -Click [![Binder](http://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/tpaviot/pythonocc-binderhub/7.9.0) to open a jupyter notebook running the latest pythonocc-core 7.9.0. +Click [![Binder](http://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/tpaviot/pythonocc-binderhub/7.9.3) to open a jupyter notebook running the latest pythonocc-core 7.9.3. Install with conda ------------------ -pythonocc provides precompiled [conda packages](https://anaconda.org/pythonocc/pythonocc-core) (they depend on third part libraries made available from the conda-forge channel) for python 3.9, 3.10, 3.11 and 3.12. This will get you up and running in minutes whether you run win32/win64/linux64/osx64. Here is an example for python 3.10: +pythonocc provides precompiled [conda packages](https://anaconda.org/pythonocc/pythonocc-core) (they depend on third part libraries made available from the conda-forge channel) for python 3.10, 3.11, 3.12, 3.13 and 3.14. This will get you up and running in minutes whether you run win32/win64/linux64/osx64. Here is an example for python 3.12: ```bash # first create an environment -conda create --name=pyoccenv python=3.10 +conda create --name=pyoccenv python=3.12 conda activate pyoccenv -conda install -c conda-forge pythonocc-core=7.9.0 +conda install -c conda-forge pythonocc-core=7.9.3 ``` Other conda channels may provide pythonocc-core packages, check [search Anaconda](https://anaconda.org/search?q=pythonocc-core) From 620039a9d870146f4b56bcd94e37050c19f99e50 Mon Sep 17 00:00:00 2001 From: Martin Siggel Date: Mon, 4 May 2026 09:38:06 +0200 Subject: [PATCH 11/12] Fixed incorrect reported package version Fixes #1488 --- src/PkgBase/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PkgBase/__init__.py b/src/PkgBase/__init__.py index d6c5e0b7e..ef8990f6f 100644 --- a/src/PkgBase/__init__.py +++ b/src/PkgBase/__init__.py @@ -5,7 +5,7 @@ # Version number PYTHONOCC_VERSION_MAJOR = 7 PYTHONOCC_VERSION_MINOR = 9 -PYTHONOCC_VERSION_PATCH = 0 +PYTHONOCC_VERSION_PATCH = 3 # Empty for official releases, set to -dev, -rc1, etc for development releases PYTHONOCC_VERSION_DEVEL = "" From 1a1e20c3bfffb5ee741144302537f3296da0bdca Mon Sep 17 00:00:00 2001 From: Martin Siggel Date: Mon, 4 May 2026 10:34:29 +0200 Subject: [PATCH 12/12] Prepare bump-my-version support --- .bumpversion.toml | 67 +++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- src/PkgBase/__init__.py | 2 +- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 .bumpversion.toml diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 000000000..0cb6dc788 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,67 @@ +[tool.bumpversion] +current_version = "7.9.3" +commit = true +tag = false +tag_name = "{new_major}.{new_minor}.{new_patch}" + +[[tool.bumpversion.files]] +filename = "README.md" +search = "pythonocc-binderhub/{current_version}" +replace = "pythonocc-binderhub/{new_version}" + + +[[tool.bumpversion.files]] +filename = "README.md" +regex = true +search = "Latest release: \\[pythonocc-core {current_version} \\(\\d{{4}}-\\d{{2}}-\\d{{2}}\\)\\]\\(https://github\\.com/tpaviot/pythonocc-core/releases/tag/{current_version}\\)" +replace = "Latest release: [pythonocc-core {new_version} ({now:%Y-%m-%d})](https://github.com/tpaviot/pythonocc-core/releases/tag/{new_version})" + +[[tool.bumpversion.files]] +filename = "README.md" +search = "to open a jupyter notebook running the latest pythonocc-core {current_version}." +replace = "to open a jupyter notebook running the latest pythonocc-core {new_version}." + +[[tool.bumpversion.files]] +filename = "README.md" +search = "conda install -c conda-forge pythonocc-core={current_version}" +replace = "conda install -c conda-forge pythonocc-core={new_version}" + +[[tool.bumpversion.files]] +filename = "CMakeLists.txt" +search = "VERSION_MAJOR {current_major}" +replace = "VERSION_MAJOR {new_major}" + +[[tool.bumpversion.files]] +filename = "CMakeLists.txt" +search = "VERSION_MINOR {current_minor}" +replace = "VERSION_MINOR {new_minor}" + +[[tool.bumpversion.files]] +filename = "CMakeLists.txt" +search = "VERSION_PATCH {current_patch}" +replace = "VERSION_PATCH {new_patch}" + +[[tool.bumpversion.files]] +filename = "src/PkgBase/__init__.py" +search = "PYTHONOCC_VERSION_MAJOR = {current_major}" +replace = "PYTHONOCC_VERSION_MAJOR = {new_major}" + +[[tool.bumpversion.files]] +filename = "src/PkgBase/__init__.py" +search = "PYTHONOCC_VERSION_MINOR = {current_minor}" +replace = "PYTHONOCC_VERSION_MINOR = {new_minor}" + +[[tool.bumpversion.files]] +filename = "src/PkgBase/__init__.py" +search = "PYTHONOCC_VERSION_PATCH = {current_patch}" +replace = "PYTHONOCC_VERSION_PATCH = {new_patch}" + +[[tool.bumpversion.files]] +filename = "ci/conda/meta.yaml" +search = "set version = \"{current_version}\"" +replace = "set version = \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "ci/conda/meta.yaml" +search = "- occt =={current_version}" +replace = "- occt =={new_version}" \ No newline at end of file diff --git a/README.md b/README.md index d0b13b141..6c0bb30b3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ About ----- pythonocc provides 3D modeling and dataexchange features. It is intended for CAD/PDM/PLM/BIM development. It is based on the OpenCascade Technology modeling kernel. -Latest release: [pythonocc-core 7.9.3 (February 2026)](https://github.com/tpaviot/pythonocc-core/releases/tag/7.9.3) +Latest release: [pythonocc-core 7.9.3 (2026-02-12)](https://github.com/tpaviot/pythonocc-core/releases/tag/7.9.3) Features -------- diff --git a/src/PkgBase/__init__.py b/src/PkgBase/__init__.py index d6c5e0b7e..ef8990f6f 100644 --- a/src/PkgBase/__init__.py +++ b/src/PkgBase/__init__.py @@ -5,7 +5,7 @@ # Version number PYTHONOCC_VERSION_MAJOR = 7 PYTHONOCC_VERSION_MINOR = 9 -PYTHONOCC_VERSION_PATCH = 0 +PYTHONOCC_VERSION_PATCH = 3 # Empty for official releases, set to -dev, -rc1, etc for development releases PYTHONOCC_VERSION_DEVEL = ""