openscad-cpp-evaluator 1.24.0__tar.gz → 1.25.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/CLAUDE.md +34 -0
  2. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/mesh_check.hpp +13 -3
  4. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/pyproject.toml +1 -1
  5. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/builtins.hpp +4 -0
  6. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/function_builtins.cpp +1 -0
  7. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/import.cpp +23 -2
  8. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/primitives_3d.cpp +9 -1
  9. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/registry.cpp +4 -1
  10. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/topology.cpp +112 -0
  11. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/mesh_check.cpp +11 -6
  12. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_booleans.cpp +96 -0
  13. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_csg_tree.cpp +21 -0
  14. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/.github/workflows/ci.yml +0 -0
  15. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/.github/workflows/wheels.yml +0 -0
  16. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/.gitignore +0 -0
  17. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/.gitmodules +0 -0
  18. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/CMakeLists.txt +0 -0
  19. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/LICENSE +0 -0
  20. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/README.md +0 -0
  21. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/bindings/CMakeLists.txt +0 -0
  22. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/bindings/ast_to_py.cpp +0 -0
  23. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/bindings/module.cpp +0 -0
  24. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/cmake/embed_font.cmake +0 -0
  25. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/examples/CMakeLists.txt +0 -0
  26. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/examples/eval_perf_benchmark.cpp +0 -0
  27. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/examples/manifold_cache_reuse.cpp +0 -0
  28. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/examples/minimal_debugger.cpp +0 -0
  29. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  30. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/.gitignore +0 -0
  31. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  32. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  33. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/README.md +0 -0
  34. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  35. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  36. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  37. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  38. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  39. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  40. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  41. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  42. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  43. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  44. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  45. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  46. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  47. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  48. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  49. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  50. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  51. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  52. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  53. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  54. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  55. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  56. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  57. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  58. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  59. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  60. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  61. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  62. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  63. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  64. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  65. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  66. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  67. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  68. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  69. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  70. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  71. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  72. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  73. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  74. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  75. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  76. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  77. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  78. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  79. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  80. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  81. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  82. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  83. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  84. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  85. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  86. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  87. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  88. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  89. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  90. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  91. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  92. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  93. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  94. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  95. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  96. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  97. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  98. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  99. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  100. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  101. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  102. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  103. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  104. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  105. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  106. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  107. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  108. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  109. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  110. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  111. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  112. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  113. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  114. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  115. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  116. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  117. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  118. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  119. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  120. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  121. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  122. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  123. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  124. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  125. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  126. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  127. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  128. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  129. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  130. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  131. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  132. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  133. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  134. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LICENSE +0 -0
  135. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  136. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  137. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  138. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  139. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/CMakeLists.txt +0 -0
  140. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/booleans.cpp +0 -0
  141. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/call_args.cpp +0 -0
  142. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/color.cpp +0 -0
  143. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/control.cpp +0 -0
  144. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/dxf_dim.cpp +0 -0
  145. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/extrude.cpp +0 -0
  146. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/modifiers.cpp +0 -0
  147. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/primitives_2d.cpp +0 -0
  148. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/roof.cpp +0 -0
  149. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/segments.cpp +0 -0
  150. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/surface.cpp +0 -0
  151. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/text.cpp +0 -0
  152. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/builtins/transforms.cpp +0 -0
  153. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/bytecode_compiler.cpp +0 -0
  154. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/bytecode_vm.cpp +0 -0
  155. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/colored_body.cpp +0 -0
  156. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/coverage.cpp +0 -0
  157. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/csg_generate.cpp +0 -0
  158. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/csg_resolve.cpp +0 -0
  159. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/css_colors.cpp +0 -0
  160. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/debug/debug_repl.cpp +0 -0
  161. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/debug_profile.cpp +0 -0
  162. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/eval_context.cpp +0 -0
  163. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/eval_error.cpp +0 -0
  164. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/eval_use.cpp +0 -0
  165. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/export.cpp +0 -0
  166. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/expr_eval.cpp +0 -0
  167. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/format_closure.cpp +0 -0
  168. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/import/dxf_import.cpp +0 -0
  169. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/import/mesh_import.cpp +0 -0
  170. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/import/surface_load.cpp +0 -0
  171. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/import/svg_import.cpp +0 -0
  172. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/manifold_cache.cpp +0 -0
  173. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/measure_geometry.cpp +0 -0
  174. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/stmt_eval.cpp +0 -0
  175. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/text/font_match.cpp +0 -0
  176. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/text/freetype_font_provider.cpp +0 -0
  177. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/text/text_metrics.cpp +0 -0
  178. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/user_calls.cpp +0 -0
  179. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/utf8.cpp +0 -0
  180. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/value.cpp +0 -0
  181. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/src/zip_stored.cpp +0 -0
  182. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/CMakeLists.txt +0 -0
  183. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_bytecode_compiler.cpp +0 -0
  184. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_children_separate.cpp +0 -0
  185. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_cli.cpp +0 -0
  186. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_control_flow.cpp +0 -0
  187. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_coverage.cpp +0 -0
  188. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_debug_hooks.cpp +0 -0
  189. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_dxf_svg_import.cpp +0 -0
  190. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_eval_context.cpp +0 -0
  191. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_export.cpp +0 -0
  192. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_expr_eval.cpp +0 -0
  193. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_extrude_roof.cpp +0 -0
  194. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_feature_detection.cpp +0 -0
  195. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_function_builtins.cpp +0 -0
  196. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_helpers.hpp +0 -0
  197. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_import_export.cpp +0 -0
  198. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_include_cache.cpp +0 -0
  199. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_manifold_cache.cpp +0 -0
  200. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_mesh_check.cpp +0 -0
  201. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_multi_color_merge.cpp +0 -0
  202. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_primitives.cpp +0 -0
  203. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_profiling.cpp +0 -0
  204. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_python_bindings.py +0 -0
  205. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_render_expr.cpp +0 -0
  206. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_scope_trail.cpp +0 -0
  207. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_scoping.cpp +0 -0
  208. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_surface.cpp +0 -0
  209. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_tail_calls.cpp +0 -0
  210. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_text.cpp +0 -0
  211. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_transforms.cpp +0 -0
  212. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_unexpected_args.cpp +0 -0
  213. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_use.cpp +0 -0
  214. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_value.cpp +0 -0
  215. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_viewport_params.cpp +0 -0
  216. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tests/test_zip_stored.cpp +0 -0
  217. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tools/cli/CMakeLists.txt +0 -0
  218. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tools/cli/cli_lib.cpp +0 -0
  219. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tools/cli/cli_lib.hpp +0 -0
  220. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/tools/cli/main.cpp +0 -0
  221. {openscad_cpp_evaluator-1.24.0 → openscad_cpp_evaluator-1.25.0}/uv.lock +0 -0
@@ -946,6 +946,40 @@ grep for `ponytail:`.
946
946
  shut. Note the reduction is NOT monotonic in the tolerance (1.0 gave *more* triangles than 0.5 on
947
947
  the test model, since simplification can force re-triangulation), so don't read a jump as a bug.
948
948
 
949
+ - `mesh_repair()` (also `topology.cpp`) — makes a broken mesh watertight, so geometry that was
950
+ merely *almost* closed can take part in CSG. Another BelfrySCAD extension: nothing in the language
951
+ could repair geometry, and a mesh from a library generator or an import is routinely a few
952
+ boundary edges short of a solid. The work is `repairMesh()`'s, already used by
953
+ `import(..., repair=true)`; this exposes the same six steps to any geometry — weld, drop
954
+ degenerate/duplicate, orient, fill holes, flip outward, strip zero-area faces.
955
+
956
+ **It reads `ColoredBody::rawMesh`, not `body`.** An open `polyhedron()` cannot build a Manifold,
957
+ so it keeps the raw triangle soup there and leaves `body` set but EMPTY. Repairing the empty
958
+ Manifold finds nothing to do and reports success at having done nothing, which is how the first
959
+ version of this failed silently. On success the soup is cleared, or `splitByRole()` keeps pulling
960
+ the body aside as un-CSG-able — the very thing the repair just fixed.
961
+
962
+ **The default tolerance is `GRID_FINE` (2⁻²⁰ ≈ 9.54e-7)**, OpenSCAD's own constant for deciding
963
+ two points are one, so a mesh that welds there welds here; it is exact in binary, so the
964
+ multiply-and-round in `weldMap()` adds no error. Deliberately tight, unlike `simplify()`'s
965
+ scale-relative default, because welding DISCARDS vertices: a default that quietly merged real
966
+ detail would be worse than one that leaves a mesh open, and hole filling closes what welding
967
+ declines to. `import(..., repair=true)` takes the same `tolerance=` argument for the same reason.
968
+
969
+ **Watertight is not the same as clean**, and the tolerance chooses. A gap whose rim is two
970
+ vertices that should be one closes either by welding (the duplicate disappears, no new geometry)
971
+ or by filling (the pair survives and step 4 stitches a triangle across). The filled patch is a
972
+ *needle* — two corners a hair apart — and step 6 will not remove it, because it strips
973
+ genuinely zero-area faces and a needle across a 2e-5 gap with its third corner 0.25 away has an
974
+ area around 2.5e-6. Thin, not degenerate. So a repaired-but-ugly mesh means the tolerance was too
975
+ small; the `polyhedron()` warning names a boundary edge's endpoints, so measure the gap and pass
976
+ a little more. A boundary loop of fewer than three edges — a crack, not a hole — has no fan to
977
+ build at all and is reported as left open; only a coarser weld closes that.
978
+
979
+ Not a substitute for a generator that emits a closed mesh: repair costs a full pass over the
980
+ geometry every render. Prompted by BelfrySCAD #521, where BOSL2's `isosurface()` computed a shared
981
+ voxel corner once per voxel and let the results drift 2e-5 apart.
982
+
949
983
  - `src/builtins/dxf_dim.cpp` — `dxf_dim()`/`dxf_cross()`, which read a measurement out of a DXF file
950
984
  rather than out of the model. Ported from the reference's `io/dxfdim.cc` plus the DIMENSION/LINE
951
985
  half of `io/DxfData.cc`: `dxf_dim()` returns the first matching DIMENSION's value by its type
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.24.0
3
+ Version: 1.25.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -82,6 +82,15 @@ struct MeshRepairReport {
82
82
  std::string summary() const;
83
83
  };
84
84
 
85
+ //: Default welding tolerance: OpenSCAD's own GRID_FINE, 2^-20. Theirs is
86
+ //: the constant their Grid.h uses to decide two points are one, so a mesh
87
+ //: that welds there welds here. Exact in binary, so the multiply-and-round
88
+ //: in weldMap() adds no error of its own. Deliberately tight: welding
89
+ //: DISCARDS vertices, and a default that quietly merged real detail would
90
+ //: be worse than one that leaves a mesh open -- hole filling closes what
91
+ //: welding declines to. See issue #190.
92
+ constexpr double kDefaultWeldTolerance = 0.00000095367431640625;
93
+
85
94
  // Best-effort repair, in the only order that works: welding first (it
86
95
  // changes which edges are shared, so every later test depends on it), then
87
96
  // degenerate and duplicate removal, then orientation, then hole filling
@@ -91,9 +100,10 @@ struct MeshRepairReport {
91
100
  // Returns the repaired mesh. A mesh it cannot close is still returned,
92
101
  // improved as far as it got, with the remainder reported in `report`.
93
102
  template <typename M>
94
- M repairMesh(const M& mesh, MeshRepairReport& report);
95
- extern template manifold::MeshGL repairMesh<manifold::MeshGL>(const manifold::MeshGL&, MeshRepairReport&);
96
- extern template manifold::MeshGL64 repairMesh<manifold::MeshGL64>(const manifold::MeshGL64&, MeshRepairReport&);
103
+ M repairMesh(const M& mesh, MeshRepairReport& report,
104
+ double tolerance = kDefaultWeldTolerance);
105
+ extern template manifold::MeshGL repairMesh<manifold::MeshGL>(const manifold::MeshGL&, MeshRepairReport&, double);
106
+ extern template manifold::MeshGL64 repairMesh<manifold::MeshGL64>(const manifold::MeshGL64&, MeshRepairReport&, double);
97
107
 
98
108
  struct SliverStripReport {
99
109
  size_t removed = 0; // zero-area faces taken out
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.24.0"
7
+ version = "1.25.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -188,6 +188,10 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
188
188
  const std::vector<std::unique_ptr<CSGNode>>& children,
189
189
  const oscad::ASTNode& node);
190
190
 
191
+ CSGParams resolveMeshRepair(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
192
+ std::vector<ColoredBody> generateMeshRepair(Evaluator& ev, const CSGParams& params,
193
+ const std::vector<std::unique_ptr<CSGNode>>& children,
194
+ const oscad::ASTNode& node);
191
195
  CSGParams resolveSimplify(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
192
196
  std::vector<ColoredBody> generateSimplify(Evaluator& ev, const CSGParams& params,
193
197
  const std::vector<std::unique_ptr<CSGNode>>& children,
@@ -1011,6 +1011,7 @@ const std::unordered_map<std::string, double>& featureLevels() {
1011
1011
  {"sphere-styles", 1.0}, // sphere(style=)
1012
1012
  {"export-name", 1.0}, // $export_name
1013
1013
  {"simplify-op", 1.0}, // simplify()
1014
+ {"mesh-repair", 1.0}, // mesh_repair(), and import(tolerance=)
1014
1015
  {"expr-import", 1.0}, // import() in expression position
1015
1016
  {"object-function", 1.0}, // object(), unconditional here
1016
1017
  {"roof-op", 1.0}, // roof(), method="voronoi" only
@@ -186,6 +186,10 @@ CSGParams resolveImport(Evaluator& ev, const oscad::ModularCall& node, EvalConte
186
186
  // Not an OpenSCAD parameter. A script using it will not run upstream,
187
187
  // which is why it has to be asked for rather than being the default.
188
188
  const bool repair = truthy(getArg(args, std::nullopt, "repair", Value{false}));
189
+ // How far apart two vertices may be and still be welded into one.
190
+ // Named rather than hidden so import() and mesh_repair() answer the
191
+ // same question the same way (issue #190); undef keeps the default.
192
+ const Value toleranceArg = getArg(args, std::nullopt, "tolerance", Value{});
189
193
 
190
194
  CSGParams params;
191
195
  params["color"] = colorToValue(effCtx.color);
@@ -255,6 +259,7 @@ CSGParams resolveImport(Evaluator& ev, const oscad::ModularCall& node, EvalConte
255
259
  }
256
260
  params["kind"] = Value{std::string("mesh")};
257
261
  params["repair"] = Value{repair};
262
+ params["tolerance"] = toleranceArg;
258
263
  params["verts"] = Value{std::make_shared<const ValueList>(ValueList{std::move(vertsFlat)})};
259
264
  params["tris"] = Value{std::make_shared<const ValueList>(ValueList{std::move(trisFlat)})};
260
265
  return params;
@@ -315,9 +320,23 @@ std::vector<ColoredBody> generateImport(Evaluator& ev, const CSGParams& params,
315
320
  const auto repairIt = params.find("repair");
316
321
  const bool repair = repairIt != params.end() && truthy(repairIt->second);
317
322
  if (repair) {
323
+ double tolerance = kDefaultWeldTolerance;
324
+ const auto tolIt = params.find("tolerance");
325
+ if (tolIt != params.end()) {
326
+ if (const double* t = std::get_if<double>(&tolIt->second)) {
327
+ if (*t < 0.0) {
328
+ ev.warn("import: tolerance must not be negative; using the default",
329
+ &node.position());
330
+ } else {
331
+ tolerance = *t;
332
+ }
333
+ } else if (!std::holds_alternative<std::monostate>(tolIt->second)) {
334
+ ev.warn("import: tolerance must be a number", &node.position());
335
+ }
336
+ }
318
337
  const MeshDiagnosis before = checkMesh(mesh);
319
338
  MeshRepairReport rep;
320
- manifold::MeshGL64 fixed = repairMesh(mesh, rep);
339
+ manifold::MeshGL64 fixed = repairMesh(mesh, rep, tolerance);
321
340
  const MeshDiagnosis after = checkMesh(fixed);
322
341
  if (rep.didAnything()) {
323
342
  ev.warn("import: repaired the mesh -- " + rep.summary(), &node.position());
@@ -344,7 +363,9 @@ std::vector<ColoredBody> generateImport(Evaluator& ev, const CSGParams& params,
344
363
  if (why.empty()) why = manifoldErrorName(body.Status());
345
364
  if (!ev.insideHull) {
346
365
  ev.warn("import: mesh is not a closed solid (" + why +
347
- "); drawing it as a surface. hull() can still use its points, but "
366
+ // Same rewording as polyhedron's: see the comment there.
367
+ "); drawing the object as an open surface rather than a solid "
368
+ "-- nothing is patched. hull() can still use its points, but "
348
369
  "it cannot take part in union/difference/intersection" +
349
370
  std::string(repair ? "" : ". Try import(..., repair=true)"),
350
371
  &node.position());
@@ -854,7 +854,15 @@ std::vector<ColoredBody> generatePolyhedron(Evaluator& ev, const CSGParams& para
854
854
  open.first ? ", first at " + describeEdge(mesh, *open.first) : std::string();
855
855
  ev.warn("polyhedron: mesh is not closed -- " + std::to_string(open.count) +
856
856
  " boundary edge(s)" + where +
857
- "; drawing it as a surface. hull() can still "
857
+ // "drawing it as a surface" was read by a reporter as
858
+ // the HOLE being surfaced over -- i.e. as automatic
859
+ // repair hiding their defect. Nothing is repaired: the
860
+ // mesh is drawn as the open surface it is. Name the
861
+ // object as the thing being drawn, and say what it is
862
+ // NOT, since "surface" alone does not imply "not solid"
863
+ // to someone who did not know it could be either.
864
+ "; drawing the object as an open surface rather than a "
865
+ "solid -- nothing is patched. hull() can still "
858
866
  "use its points, but it cannot take part in union/difference/intersection",
859
867
  &node.position());
860
868
  }
@@ -37,6 +37,7 @@ const std::unordered_map<std::string_view, ResolveFn>& resolveDispatch() {
37
37
  {"fill", &resolveFill},
38
38
  {"minkowski_difference", &resolveMinkowskiDifference},
39
39
  {"simplify", &resolveSimplify},
40
+ {"mesh_repair", &resolveMeshRepair},
40
41
  {"levelset", &resolveLevelSet},
41
42
  {"offset", &resolveOffset},
42
43
  {"surface", &resolveSurface},
@@ -79,6 +80,7 @@ const std::unordered_map<std::string_view, GenerateFn>& generateDispatch() {
79
80
  {"fill", &generateFill},
80
81
  {"minkowski_difference", &generateMinkowskiDifference},
81
82
  {"simplify", &generateSimplify},
83
+ {"mesh_repair", &generateMeshRepair},
82
84
  {"levelset", &generateLevelSet},
83
85
  {"offset", &generateOffset},
84
86
  {"surface", &generateSurface},
@@ -122,7 +124,7 @@ const std::vector<std::string>* builtinParamNames(const std::string& name) {
122
124
  // full and quietly given the whole drawing.)
123
125
  {"import",
124
126
  {"file", "layer", "convexity", "origin", "scale", "width", "height", "filename", "layername", "center", "dpi",
125
- "id", "class", "repair"}},
127
+ "id", "class", "repair", "tolerance"}},
126
128
  {"linear_extrude", {"height", "v", "scale", "center", "twist", "slices", "segments", "convexity"}},
127
129
  {"rotate_extrude", {"angle", "start", "convexity"}},
128
130
  {"projection", {"cut", "convexity"}},
@@ -130,6 +132,7 @@ const std::vector<std::string>* builtinParamNames(const std::string& name) {
130
132
  {"fill", {}},
131
133
  {"minkowski_difference", {}},
132
134
  {"simplify", {"tolerance"}},
135
+ {"mesh_repair", {"tolerance"}},
133
136
  {"levelset", {"field", "bounds", "isovalue", "invert", "edge"}},
134
137
  {"offset", {"r", "delta", "chamfer"}},
135
138
  {"surface", {"file", "center", "convexity", "invert"}},
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include "openscad_cpp_evaluator/call_args.hpp"
4
4
  #include "openscad_cpp_evaluator/evaluator.hpp"
5
+ #include "openscad_cpp_evaluator/mesh_check.hpp"
5
6
 
6
7
  #include <manifold/polygon.h>
7
8
 
@@ -503,6 +504,117 @@ std::vector<ColoredBody> generateSimplify(Evaluator& ev, const CSGParams& params
503
504
  return out;
504
505
  }
505
506
 
507
+ // mesh_repair(tolerance) -- make a broken mesh watertight. A BelfrySCAD
508
+ // extension: nothing in the OpenSCAD language can repair geometry, and a
509
+ // mesh arriving from a library generator or an import is routinely almost
510
+ // closed rather than closed. See issue #190.
511
+ //
512
+ // The work is repairMesh()'s, already used by import(..., repair=true);
513
+ // this exposes the same six steps to any geometry, not only imported
514
+ // files: weld, drop degenerate/duplicate, orient, fill holes, flip
515
+ // outward, strip zero-area faces.
516
+ //
517
+ // NOT a substitute for a generator that emits a closed mesh. Repair costs
518
+ // a full pass over the geometry every render, and welding discards
519
+ // vertices -- which is why the default tolerance is tight enough to
520
+ // weld only what is unambiguously one point, leaving hole filling to
521
+ // close the rest.
522
+
523
+ CSGParams resolveMeshRepair(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
524
+ auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
525
+ CSGParams params;
526
+ // Position 0, so mesh_repair(0.001) reads the way simplify(0.1) does.
527
+ params["tolerance"] = getArg(args, 0, "tolerance", Value{});
528
+ EvalContext blockCtx = ev.blockScope(effCtx);
529
+ ev.evalChildren(node.children, blockCtx);
530
+ return params;
531
+ }
532
+
533
+ std::vector<ColoredBody> generateMeshRepair(Evaluator& ev, const CSGParams& params,
534
+ const std::vector<std::unique_ptr<CSGNode>>& children,
535
+ const oscad::ASTNode& node) {
536
+ const std::vector<ColoredBody> bodies = flattenCsgTree(children);
537
+ if (bodies.empty()) return {};
538
+
539
+ const Value tolArg = params.at("tolerance");
540
+ double tolerance = kDefaultWeldTolerance;
541
+ if (std::holds_alternative<double>(tolArg)) {
542
+ tolerance = std::get<double>(tolArg);
543
+ if (tolerance < 0.0) {
544
+ ev.warn("mesh_repair: tolerance must not be negative", &node.position());
545
+ return bodies;
546
+ }
547
+ } else if (!std::holds_alternative<std::monostate>(tolArg)) {
548
+ ev.warn("mesh_repair: tolerance must be a number", &node.position());
549
+ }
550
+
551
+ std::vector<ColoredBody> out;
552
+ out.reserve(bodies.size());
553
+ for (const ColoredBody& b : bodies) {
554
+ // Background/highlight geometry is a debugging aid, not part of the
555
+ // model; repairing it would spend the pass and say nothing useful.
556
+ // A 2D section has no mesh to repair either.
557
+ if (b.role != BodyRole::Normal || (!b.body && !b.rawMesh)) {
558
+ out.push_back(b);
559
+ continue;
560
+ }
561
+ // The case this module exists for: polyhedron() could not build a
562
+ // Manifold from an open mesh, so it kept the raw soup in rawMesh
563
+ // and left `body` set but EMPTY (see ColoredBody::rawMesh). Repair
564
+ // the soup -- repairing the empty Manifold would find nothing to
565
+ // do and report success at having done nothing.
566
+ MeshRepairReport report;
567
+ ColoredBody cb = b;
568
+ manifold::MeshGL64 fixed;
569
+ if (b.rawMesh) {
570
+ manifold::MeshGL64 soup;
571
+ soup.numProp = b.rawMesh->numProp;
572
+ soup.vertProperties.assign(b.rawMesh->vertProperties.begin(),
573
+ b.rawMesh->vertProperties.end());
574
+ soup.triVerts.assign(b.rawMesh->triVerts.begin(), b.rawMesh->triVerts.end());
575
+ fixed = repairMesh(soup, report, tolerance);
576
+ } else {
577
+ fixed = repairMesh(b.body->GetMeshGL64(), report, tolerance);
578
+ }
579
+ if (report.didAnything()) {
580
+ manifold::Manifold rebuilt(fixed);
581
+ if (rebuilt.Status() == manifold::Manifold::Error::NoError) {
582
+ cb.body = std::move(rebuilt);
583
+ // It is a real solid now, so stop carrying the display-only
584
+ // soup: leaving it set would keep splitByRole() pulling the
585
+ // body aside as un-CSG-able, which is the whole thing the
586
+ // repair just fixed.
587
+ cb.rawMesh.reset();
588
+ // triColors is indexed by triangle, and repair changes how
589
+ // many there are -- filling adds, sliver stripping removes.
590
+ // Carrying it across would mask an N-triangle mesh with an
591
+ // M-entry array, which is an IndexError rather than a wrong
592
+ // colour. Same reasoning as simplify()'s.
593
+ cb.triColors.reset();
594
+ ev.warn("mesh_repair: " + report.summary(), &node.position());
595
+ } else {
596
+ // Say what it could not do. Silently returning the original
597
+ // would leave the author believing a repair happened.
598
+ ev.warn("mesh_repair: could not rebuild the repaired mesh (" +
599
+ std::string(manifoldErrorName(rebuilt.Status())) +
600
+ "); geometry left unchanged",
601
+ &node.position());
602
+ }
603
+ }
604
+ if (report.unfilledHoles) {
605
+ // The sub-3-edge boundary loop: a crack between two nearly
606
+ // coincident vertices, with no fan to build. Only a coarser
607
+ // weld closes it, so name the knob rather than just the count.
608
+ ev.warn("mesh_repair: " + std::to_string(report.unfilledHoles) +
609
+ " hole(s) left open -- a crack too narrow to fill needs a"
610
+ " larger tolerance to weld shut instead",
611
+ &node.position());
612
+ }
613
+ out.push_back(std::move(cb));
614
+ }
615
+ return out;
616
+ }
617
+
506
618
  // -- levelset(field, bounds, isovalue) -------------------------------------
507
619
  //
508
620
  // A solid from a grid of sampled values. Wraps Manifold::LevelSet with a C++
@@ -309,7 +309,7 @@ namespace {
309
309
  // this: unwelded duplicates split one shared edge into two boundary edges,
310
310
  // so a mesh that is really closed reads as full of holes until it is done.
311
311
  template <typename M>
312
- std::vector<Vert> weldMap(const M& m, size_t& welded) {
312
+ std::vector<Vert> weldMap(const M& m, size_t& welded, double tolerance) {
313
313
  const size_t stride = m.numProp ? m.numProp : 3;
314
314
  const size_t n = stride ? m.vertProperties.size() / stride : 0;
315
315
  std::vector<Vert> remap(n);
@@ -332,12 +332,17 @@ std::vector<Vert> weldMap(const M& m, size_t& welded) {
332
332
  }
333
333
  bool samePos(const Keyed& o) const { return x == o.x && y == o.y && z == o.z; }
334
334
  };
335
+ // Snap to a grid of `tolerance`. Guarded because a caller can pass 0 or
336
+ // a negative to mean "weld only exact duplicates" -- dividing by that
337
+ // would give inf/NaN keys and weld the whole mesh into one point.
338
+ const double inv = tolerance > 0.0 ? 1.0 / tolerance : 0.0;
335
339
  std::vector<Keyed> keyed;
336
340
  keyed.reserve(n);
337
341
  for (size_t v = 0; v < n; ++v) {
338
342
  double p[3];
339
343
  pos(m, static_cast<Vert>(v), p);
340
- keyed.push_back({llround(p[0] * 1e6), llround(p[1] * 1e6), llround(p[2] * 1e6), static_cast<Vert>(v)});
344
+ keyed.push_back({llround(p[0] * inv), llround(p[1] * inv), llround(p[2] * inv),
345
+ static_cast<Vert>(v)});
341
346
  }
342
347
  std::sort(keyed.begin(), keyed.end());
343
348
  for (size_t i = 0; i < keyed.size();) {
@@ -400,14 +405,14 @@ std::vector<std::vector<Vert>> boundaryLoops(const std::vector<std::array<Vert,
400
405
  } // namespace
401
406
 
402
407
  template <typename M>
403
- M repairMesh(const M& mesh, MeshRepairReport& report) {
408
+ M repairMesh(const M& mesh, MeshRepairReport& report, double tolerance) {
404
409
  report = MeshRepairReport{};
405
410
  M out = mesh;
406
411
 
407
412
  // 1. Weld. Must come first -- it decides which edges are shared, and
408
413
  // every later step reads the edge map.
409
414
  size_t welded = 0;
410
- const std::vector<Vert> remap = weldMap(mesh, welded);
415
+ const std::vector<Vert> remap = weldMap(mesh, welded, tolerance);
411
416
  report.weldedVertices = welded;
412
417
 
413
418
  // Remap every face, then find the duplicates by sorting rather than by
@@ -850,8 +855,8 @@ M stripSlivers(const M& mesh, SliverStripReport& report) {
850
855
  // header, keeps compile times where they were.
851
856
  template MeshDiagnosis checkMesh<manifold::MeshGL>(const manifold::MeshGL&);
852
857
  template MeshDiagnosis checkMesh<manifold::MeshGL64>(const manifold::MeshGL64&);
853
- template manifold::MeshGL repairMesh<manifold::MeshGL>(const manifold::MeshGL&, MeshRepairReport&);
854
- template manifold::MeshGL64 repairMesh<manifold::MeshGL64>(const manifold::MeshGL64&, MeshRepairReport&);
858
+ template manifold::MeshGL repairMesh<manifold::MeshGL>(const manifold::MeshGL&, MeshRepairReport&, double);
859
+ template manifold::MeshGL64 repairMesh<manifold::MeshGL64>(const manifold::MeshGL64&, MeshRepairReport&, double);
855
860
  template manifold::MeshGL stripSlivers<manifold::MeshGL>(const manifold::MeshGL&, SliverStripReport&);
856
861
  template manifold::MeshGL64 stripSlivers<manifold::MeshGL64>(const manifold::MeshGL64&, SliverStripReport&);
857
862
 
@@ -1660,3 +1660,99 @@ TEST(CsgTree, InnerNodeBodiesAreReleasedOnceTheParentHasConsumedThem) {
1660
1660
  Evaluated plain = evalSrc("union() { cube(1); sphere(1); }"); // the union really was consumed first
1661
1661
  EXPECT_NEAR(e.bodies[0].body->Volume(), plain.bodies[0].body->Volume(), 1e-9);
1662
1662
  }
1663
+
1664
+ // -- mesh_repair() ----------------------------------------------------------
1665
+ //
1666
+ // Issue #190, out of BelfrySCAD #521: a mesh that is almost closed is not a
1667
+ // solid, and nothing in the language could make it one. The repair itself is
1668
+ // repairMesh()'s, already covered in test_mesh_check.cpp; these cover the
1669
+ // module -- that it reaches the raw soup polyhedron() keeps, that the result
1670
+ // is a real solid, and that tolerance does what it claims.
1671
+
1672
+ namespace {
1673
+ // A 20-cube with its lid left off: five faces, four boundary edges.
1674
+ const char* kOpenCube =
1675
+ "polyhedron(points=[[-10,-10,-10],[10,-10,-10],[10,10,-10],[-10,10,-10],"
1676
+ " [-10,-10, 10],[10,-10, 10],[10,10, 10],[-10,10, 10]],"
1677
+ " faces=[[0,1,2,3],[4,7,6,5],[0,4,5,1],[1,5,6,2],[2,6,7,3]]);";
1678
+ } // namespace
1679
+
1680
+ TEST(MeshRepairModule, TurnsAnOpenMeshIntoASolid) {
1681
+ // Unrepaired, polyhedron() cannot build a Manifold at all: it keeps the
1682
+ // triangle soup for display and leaves an EMPTY body behind.
1683
+ Evaluated broken = evalSrc(kOpenCube);
1684
+ ASSERT_EQ(broken.bodies.size(), 1u);
1685
+ EXPECT_TRUE(broken.bodies[0].rawMesh.has_value());
1686
+ EXPECT_DOUBLE_EQ(broken.bodies[0].body->Volume(), 0.0);
1687
+
1688
+ Evaluated fixed = evalSrc(std::string("mesh_repair() ") + kOpenCube);
1689
+ ASSERT_EQ(fixed.bodies.size(), 1u);
1690
+ // The soup is gone: splitByRole() would otherwise keep pulling this
1691
+ // aside as un-CSG-able, which is the thing the repair just fixed.
1692
+ EXPECT_FALSE(fixed.bodies[0].rawMesh.has_value());
1693
+ EXPECT_NEAR(fixed.bodies[0].body->Volume(), 20.0 * 20 * 20, 1e-6);
1694
+ EXPECT_EQ(fixed.bodies[0].body->Status(), manifold::Manifold::Error::NoError);
1695
+ }
1696
+
1697
+ TEST(MeshRepairModule, TheResultCanTakePartInCSG) {
1698
+ // The whole point. An unrepaired open mesh is display-only.
1699
+ Evaluated cut = evalSrc(std::string("difference() { mesh_repair() ") + kOpenCube +
1700
+ " cube(10, center=true); }");
1701
+ ASSERT_EQ(cut.bodies.size(), 1u);
1702
+ EXPECT_NEAR(cut.bodies[0].body->Volume(), 20.0 * 20 * 20 - 10.0 * 10 * 10, 1e-6);
1703
+ }
1704
+
1705
+ TEST(MeshRepairModule, SaysWhatItDid) {
1706
+ std::string said;
1707
+ evalSrc(std::string("mesh_repair() ") + kOpenCube,
1708
+ [&](const std::string& m) { if (m.find("mesh_repair") != std::string::npos && said.empty()) said = m; });
1709
+ EXPECT_NE(said.find("hole filled"), std::string::npos) << said;
1710
+ }
1711
+
1712
+ TEST(MeshRepairModule, LeavesAClosedMeshAloneAndSaysNothing) {
1713
+ const char* closed = "cube(10, center=true);";
1714
+ std::string said;
1715
+ Evaluated e = evalSrc(std::string("mesh_repair() ") + closed,
1716
+ [&](const std::string& m) { if (said.empty()) said = m; });
1717
+ EXPECT_EQ(said, "");
1718
+ EXPECT_NEAR(e.bodies[0].body->Volume(), 1000.0, 1e-6);
1719
+ }
1720
+
1721
+ TEST(MeshRepairModule, ToleranceWeldsVerticesThatTheDefaultLeavesSplit) {
1722
+ // Two cubes sharing a face, with the shared corners 1e-3 apart -- far
1723
+ // above GRID_FINE, so the default welds nothing and only hole filling
1724
+ // closes it. A tolerance covering the gap welds instead, and the two
1725
+ // routes are distinguishable by triangle count: filling ADDS faces.
1726
+ const char* split =
1727
+ "polyhedron(points=[[0,0,0],[10,0,0],[10,10,0],[0,10,0],"
1728
+ " [0,0,10],[10,0,10],[10,10,10],[0.001,10,10]],"
1729
+ " faces=[[0,1,2,3],[4,7,6,5],[0,4,5,1],[1,5,6,2],[2,6,7,3],[3,7,4,0]]);";
1730
+ Evaluated tight = evalSrc(std::string("mesh_repair() ") + split);
1731
+ Evaluated loose = evalSrc(std::string("mesh_repair(0.01) ") + split);
1732
+ ASSERT_EQ(tight.bodies.size(), 1u);
1733
+ ASSERT_EQ(loose.bodies.size(), 1u);
1734
+ // Both must end up solid; the looser weld should not need more geometry
1735
+ // than the tighter one.
1736
+ EXPECT_EQ(tight.bodies[0].body->Status(), manifold::Manifold::Error::NoError);
1737
+ EXPECT_EQ(loose.bodies[0].body->Status(), manifold::Manifold::Error::NoError);
1738
+ EXPECT_LE(loose.bodies[0].body->NumTri(), tight.bodies[0].body->NumTri());
1739
+ }
1740
+
1741
+ TEST(MeshRepairModule, RejectsANegativeTolerance) {
1742
+ // polyhedron() warns about the open mesh first, so pick out ours.
1743
+ std::string said;
1744
+ evalSrc(std::string("mesh_repair(-1) ") + kOpenCube,
1745
+ [&](const std::string& m) {
1746
+ if (said.empty() && m.find("mesh_repair") != std::string::npos) said = m;
1747
+ });
1748
+ EXPECT_NE(said.find("must not be negative"), std::string::npos) << said;
1749
+ }
1750
+
1751
+ TEST(MeshRepairModule, IsAdvertisedBySupportedFeature) {
1752
+ // A portable script guards on the capability before calling the module,
1753
+ // since mesh_repair() is an extension and will not parse upstream.
1754
+ std::string said;
1755
+ evalSrc("echo(supported_feature(\"mesh-repair\"));",
1756
+ [&](const std::string& m) { if (said.empty()) said = m; });
1757
+ EXPECT_NE(said.find("1"), std::string::npos) << said;
1758
+ }
@@ -441,3 +441,24 @@ TEST(OpenMeshWarning, AClosedMeshStaysSilent) {
441
441
  [&](const std::string& m) { if (warning.empty()) warning = m; });
442
442
  EXPECT_EQ(warning, "");
443
443
  }
444
+
445
+ // The wording matters, and got this wrong once. "drawing it as a surface"
446
+ // was read by the reporter of BelfrySCAD #521 as the HOLE being surfaced
447
+ // over -- automatic repair hiding their defect -- which is the opposite of
448
+ // what happens, and it cost them a second round of confusion after the
449
+ // warning itself was fixed. The message must name the OBJECT as the thing
450
+ // drawn, and say outright that nothing is patched.
451
+ TEST(OpenMeshWarning, SaysTheObjectIsDrawnOpenAndNothingIsPatched) {
452
+ std::string warning;
453
+ evalSrc("polyhedron(points=[[0,0,0],[10,0,0],[10,10,0],[0,10,0],\n"
454
+ " [0,0,10],[10,0,10],[10,10,10],[0,10,10]],\n"
455
+ " faces=[[0,1,2,3],[0,4,5,1],[1,5,6,2],[2,6,7,3],[3,7,4,0]]);\n",
456
+ [&](const std::string& m) { if (warning.empty()) warning = m; });
457
+
458
+ EXPECT_NE(warning.find("drawing the object as an open surface"), std::string::npos)
459
+ << warning;
460
+ EXPECT_NE(warning.find("nothing is patched"), std::string::npos) << warning;
461
+ // The ambiguous phrasing must not come back: "it" has no clear referent
462
+ // here, and readers resolved it to the hole.
463
+ EXPECT_EQ(warning.find("drawing it as a surface"), std::string::npos) << warning;
464
+ }