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/CMakeLists.txt b/CMakeLists.txt index d29f1d79d..678dfba00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,15 +22,15 @@ 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) +set(PYTHONOCC_VERSION_DEVEL ) # 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}) @@ -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 # @@ -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) @@ -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/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..6c0bb30b3 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 (2026-02-12)](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) 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 1a9afbf22..8777f1332 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,16 +22,21 @@ requirements: - {{ cdt('libxi-devel') }} # [linux] - ninja - cmake - - swig ==4.3.1 + - swig ==4.4.1 + - sysroot_linux-64 >=2.28 # [linux] host: - python {{ python }} - - occt ==7.9.1 + - python_abi * *_cp313 [py==313] + - python_abi * *_cp314 [py==314] + - occt ==7.9.3 - numpy >=1.17 run: - - occt ==7.9.1 + - 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: 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 = "" 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; } diff --git a/src/Tesselator/ShapeTesselator.cpp b/src/Tesselator/ShapeTesselator.cpp index c6b0dc858..754757260 100644 --- a/src/Tesselator/ShapeTesselator.cpp +++ b/src/Tesselator/ShapeTesselator.cpp @@ -17,17 +17,21 @@ #include "ShapeTesselator.h" -#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 @@ -50,23 +54,32 @@ #include // ======================================================================== -// Face structure implementation +// Fast float-to-string helpers // ======================================================================== -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 snprintf (portable across all platforms) + inline void appendFloat(std::string& out, float f) { + char buf[32]; + 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) + 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); } @@ -76,12 +89,8 @@ 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 } void ShapeTesselator::Compute(bool compute_edges, float mesh_quality, bool parallel) { @@ -109,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; } @@ -122,18 +131,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,48 +152,93 @@ 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()); + +#ifdef _OPENMP + if (use_parallel && num_faces > 1) { + // Parallel processing: create face data in parallel, then collect results + std::vector local_results(num_faces); - if (triangulation.IsNull()) { - continue; + #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; + } + + Face face_data; + 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-empty results + for (auto& result : local_results) { + if (result.number_of_triangles > 0) { + 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; + } + + Face face_data; + ProcessSingleFace(face, triangulation, location, face_data); + + 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 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 +251,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 +299,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,66 +310,83 @@ 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); - + + 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; } } 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 @@ -299,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)); } } @@ -328,63 +425,67 @@ 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(); } - + const auto& nodes = poly_3d->Nodes(); const auto nb_nodes = poly_3d->NbNodes(); - - edge_data.reserve(nb_nodes); - + + 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; } - + // 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(); } - + const auto& indices = poly_on_tri->Nodes(); const auto nb_nodes = poly_on_tri->NbNodes(); - - edge_data.reserve(nb_nodes); - + + 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; } @@ -429,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 { @@ -442,43 +543,39 @@ 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); + + 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.insert(result.end(), { - consolidated_vertices[vertex_idx], - consolidated_vertices[vertex_idx + 1], - consolidated_vertices[vertex_idx + 2] - }); + std::memcpy(out, &consolidated_vertices[vertex_idx], 3 * sizeof(float)); + out += 3; } } - + 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); + + 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.insert(result.end(), { - consolidated_normals[normal_idx], - consolidated_normals[normal_idx + 1], - consolidated_normals[normal_idx + 2] - }); + std::memcpy(out, &consolidated_normals[normal_idx], 3 * sizeof(float)); + out += 3; } } - + return result; } @@ -486,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]; @@ -497,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; @@ -556,124 +653,112 @@ void ShapeTesselator::ObjGetTriangle(Standard_Integer trianglenum, Standard_Inte // Export functionality // ======================================================================== -namespace { - //! Format float number with epsilon handling - std::string formatFloatNumber(float f) { - const float epsilon = 1e-3f; - std::ostringstream formatted_float; - if (std::abs(f) < epsilon) { - f = 0.0f; - } - formatted_float << f; - return formatted_float.str(); - } -} - 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_ifs, str_vertices, str_normals; - std::vector vertices_idx(3); - std::vector normals_idx(3); - - // Process triangles and build vertex/normal strings + + // 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); + 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; + 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; + + 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(' '); } } - - str_ifs << "\n"; - str_ifs << "\n"; - str_ifs << "\n"; - str_ifs << "\n"; - - return str_ifs.str(); + + std::string result; + result.reserve(str_vertices.size() + str_normals.size() + 128); + result.append("\n\n\n\n"); + + 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 << ""; @@ -682,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 1efb01939..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,34 +43,26 @@ 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: // 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 - // 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 @@ -118,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 @@ -174,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 @@ -196,7 +185,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