openscad-cpp-evaluator 1.23.1__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.23.1 → openscad_cpp_evaluator-1.25.0}/CLAUDE.md +34 -0
  2. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/evaluator.hpp +12 -0
  4. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +17 -3
  5. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/mesh_check.hpp +13 -3
  6. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/pyproject.toml +1 -1
  7. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/CMakeLists.txt +34 -0
  8. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/builtins.hpp +4 -0
  9. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/function_builtins.cpp +1 -32
  10. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/import.cpp +23 -2
  11. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/primitives_3d.cpp +57 -6
  12. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/registry.cpp +4 -1
  13. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/topology.cpp +112 -12
  14. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/csg_generate.cpp +33 -3
  15. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/expr_eval.cpp +7 -3
  16. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/manifold_cache.cpp +3 -3
  17. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/mesh_check.cpp +11 -6
  18. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/value.cpp +0 -30
  19. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/CMakeLists.txt +4 -1
  20. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_booleans.cpp +96 -0
  21. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_csg_tree.cpp +70 -0
  22. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_manifold_cache.cpp +63 -0
  23. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/.github/workflows/ci.yml +0 -0
  24. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/.github/workflows/wheels.yml +0 -0
  25. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/.gitignore +0 -0
  26. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/.gitmodules +0 -0
  27. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/CMakeLists.txt +0 -0
  28. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/LICENSE +0 -0
  29. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/README.md +0 -0
  30. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/bindings/CMakeLists.txt +0 -0
  31. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/bindings/ast_to_py.cpp +0 -0
  32. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/bindings/module.cpp +0 -0
  33. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/cmake/embed_font.cmake +0 -0
  34. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/examples/CMakeLists.txt +0 -0
  35. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/examples/eval_perf_benchmark.cpp +0 -0
  36. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/examples/manifold_cache_reuse.cpp +0 -0
  37. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/examples/minimal_debugger.cpp +0 -0
  38. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  39. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/.gitignore +0 -0
  40. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  41. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  42. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/README.md +0 -0
  43. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  44. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  45. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  46. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  47. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  48. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  49. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  50. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  51. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  52. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  53. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  54. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  55. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  56. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  57. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  58. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  59. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  60. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  61. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  62. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  63. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  64. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  65. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  66. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  67. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  68. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  69. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  70. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  71. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  72. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  73. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  74. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  75. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  76. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  77. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  78. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  79. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  80. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  81. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  82. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  83. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  84. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  85. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  86. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  87. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  88. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  89. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  90. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  91. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  92. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  93. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  94. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  95. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  96. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  97. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  98. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  99. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  100. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  101. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  102. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  103. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  104. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  105. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  106. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  107. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  108. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  109. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  110. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  111. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  112. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  113. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  114. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  115. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  116. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  117. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  118. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  119. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  120. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  121. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  122. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  123. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  124. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  125. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  126. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  127. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  128. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  129. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  130. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  131. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  132. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  133. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  134. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  135. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  136. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  137. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  138. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  139. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  140. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  141. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LICENSE +0 -0
  142. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  143. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  144. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  145. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  146. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/booleans.cpp +0 -0
  147. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/call_args.cpp +0 -0
  148. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/color.cpp +0 -0
  149. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/control.cpp +0 -0
  150. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/dxf_dim.cpp +0 -0
  151. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/extrude.cpp +0 -0
  152. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/modifiers.cpp +0 -0
  153. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/primitives_2d.cpp +0 -0
  154. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/roof.cpp +0 -0
  155. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/segments.cpp +0 -0
  156. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/surface.cpp +0 -0
  157. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/text.cpp +0 -0
  158. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/builtins/transforms.cpp +0 -0
  159. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/bytecode_compiler.cpp +0 -0
  160. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/bytecode_vm.cpp +0 -0
  161. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/colored_body.cpp +0 -0
  162. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/coverage.cpp +0 -0
  163. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/csg_resolve.cpp +0 -0
  164. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/css_colors.cpp +0 -0
  165. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/debug/debug_repl.cpp +0 -0
  166. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/debug_profile.cpp +0 -0
  167. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/eval_context.cpp +0 -0
  168. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/eval_error.cpp +0 -0
  169. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/eval_use.cpp +0 -0
  170. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/export.cpp +0 -0
  171. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/format_closure.cpp +0 -0
  172. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/import/dxf_import.cpp +0 -0
  173. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/import/mesh_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/import/surface_load.cpp +0 -0
  175. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/import/svg_import.cpp +0 -0
  176. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/measure_geometry.cpp +0 -0
  177. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/stmt_eval.cpp +0 -0
  178. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/text/font_match.cpp +0 -0
  179. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/text/freetype_font_provider.cpp +0 -0
  180. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/text/text_metrics.cpp +0 -0
  181. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/user_calls.cpp +0 -0
  182. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/utf8.cpp +0 -0
  183. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/src/zip_stored.cpp +0 -0
  184. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_bytecode_compiler.cpp +0 -0
  185. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_children_separate.cpp +0 -0
  186. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_cli.cpp +0 -0
  187. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_control_flow.cpp +0 -0
  188. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_coverage.cpp +0 -0
  189. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_debug_hooks.cpp +0 -0
  190. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_dxf_svg_import.cpp +0 -0
  191. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_eval_context.cpp +0 -0
  192. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_export.cpp +0 -0
  193. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_expr_eval.cpp +0 -0
  194. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_extrude_roof.cpp +0 -0
  195. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_feature_detection.cpp +0 -0
  196. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_function_builtins.cpp +0 -0
  197. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_helpers.hpp +0 -0
  198. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_import_export.cpp +0 -0
  199. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_include_cache.cpp +0 -0
  200. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_mesh_check.cpp +0 -0
  201. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_multi_color_merge.cpp +0 -0
  202. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_primitives.cpp +0 -0
  203. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_profiling.cpp +0 -0
  204. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_python_bindings.py +0 -0
  205. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_render_expr.cpp +0 -0
  206. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_scope_trail.cpp +0 -0
  207. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_scoping.cpp +0 -0
  208. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_surface.cpp +0 -0
  209. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_tail_calls.cpp +0 -0
  210. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_text.cpp +0 -0
  211. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_transforms.cpp +0 -0
  212. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_unexpected_args.cpp +0 -0
  213. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_use.cpp +0 -0
  214. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_value.cpp +0 -0
  215. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_viewport_params.cpp +0 -0
  216. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tests/test_zip_stored.cpp +0 -0
  217. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tools/cli/CMakeLists.txt +0 -0
  218. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tools/cli/cli_lib.cpp +0 -0
  219. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tools/cli/cli_lib.hpp +0 -0
  220. {openscad_cpp_evaluator-1.23.1 → openscad_cpp_evaluator-1.25.0}/tools/cli/main.cpp +0 -0
  221. {openscad_cpp_evaluator-1.23.1 → 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.23.1
3
+ Version: 1.25.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -398,6 +398,14 @@ public:
398
398
  // than anything reentrancy-aware, since generateTreeImpl recurses
399
399
  // depth-first on one thread.
400
400
  const oscad::Position* generateWarnEntry = nullptr;
401
+ // Non-null while generateTreeImpl() is generating a subtree whose result
402
+ // it intends to cache: warn() appends each formatted line here as well as
403
+ // emitting it, so the entry can replay them on a later hit. Saved and
404
+ // restored around each node rather than left set, and the inner vector is
405
+ // merged into the outer one on the way out, so a parent's entry carries
406
+ // everything its whole subtree said -- which is what a parent-level hit
407
+ // has to reproduce, since it skips all of it. See ManifoldCache.
408
+ std::vector<std::string>* warnCapture = nullptr;
401
409
  // The same republish for CSGNode::callChain, read by tagGenerated to
402
410
  // fill idToCallChain.
403
411
  uint32_t generateCallChain = kNoCallChain;
@@ -530,6 +538,10 @@ public:
530
538
  // Public: builtins/import.cpp's not-manifold warning is emitted from a
531
539
  // free function, same reasoning as tagGenerated()/builtinChildren().
532
540
  void warn(const std::string& message, const oscad::Position* position);
541
+ // Hands one already-formatted warning line to echoFn_, recording it in
542
+ // warnCapture first when one is active. Every warning path goes through
543
+ // here, including a ManifoldCache hit replaying a cached line.
544
+ void emitWarning(const std::string& formatted);
533
545
 
534
546
  // Applies OpenSCAD's mixed-2D/3D rules to a node's children just
535
547
  // before its GenerateFn runs -- see csg_generate.cpp.
@@ -3,6 +3,7 @@
3
3
  #include "openscad_cpp_evaluator/csg_node.hpp"
4
4
 
5
5
  #include <mutex>
6
+ #include <vector>
6
7
  #include <optional>
7
8
  #include <string>
8
9
  #include <unordered_map>
@@ -22,15 +23,28 @@ namespace oscadeval {
22
23
  // threading.Lock -- a host may run renders/evaluates on a background
23
24
  // thread while another operation reads/clears the same cache). Mirrors
24
25
  // the reference's ManifoldCache class.
26
+ // What one cache entry holds. The warnings matter as much as the bodies:
27
+ // a hit skips the whole subtree's GenerateFn, and every generate-time
28
+ // diagnostic is raised *inside* one of those (polyhedron's "mesh is not
29
+ // closed", import()'s failures, the dimension rules). Cached without them,
30
+ // a second render of unchanged source silently dropped every warning the
31
+ // first one raised, so a real defect looked intermittent and the author
32
+ // went hunting for a phantom. A cache has to be transparent; that includes
33
+ // what it says, not only what it draws. See BelfrySCAD issue #521.
34
+ struct CachedSubtree {
35
+ std::vector<ColoredBody> bodies;
36
+ std::vector<std::string> warnings; // already formatted, replayed verbatim
37
+ };
38
+
25
39
  class ManifoldCache {
26
40
  public:
27
- std::optional<std::vector<ColoredBody>> get(const std::string& key) const;
28
- void put(std::string key, std::vector<ColoredBody> bodies);
41
+ std::optional<CachedSubtree> get(const std::string& key) const;
42
+ void put(std::string key, std::vector<ColoredBody> bodies, std::vector<std::string> warnings);
29
43
  void clear();
30
44
 
31
45
  private:
32
46
  mutable std::mutex mutex_;
33
- std::unordered_map<std::string, std::vector<ColoredBody>> entries_;
47
+ std::unordered_map<std::string, CachedSubtree> entries_;
34
48
  };
35
49
 
36
50
  // Structural content-hash key for `node`, used by generateTree()'s
@@ -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.23.1"
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"
@@ -69,3 +69,37 @@ target_link_libraries(openscad_cpp_evaluator PRIVATE linenoise freetype harfbuzz
69
69
  target_compile_options(openscad_cpp_evaluator PRIVATE
70
70
  $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall;-Wextra>
71
71
  )
72
+
73
+ # -Wall -Wextra above is about OUR code, but two TUs pull a vendored header
74
+ # in wholesale and inherit its warnings: 27 from Boost.Polygon's voronoi
75
+ # headers and 9 from stb_image_write's implementation, against 5 that were
76
+ # real. Silenced per source file, per warning.
77
+ #
78
+ # Deliberately NOT -isystem, which looks like the tidier fix and is a trap:
79
+ # clang searches -isystem AFTER /usr/local/include, so a Homebrew Boost
80
+ # 1.90 silently won over the pinned 1.86 (measured, not assumed). Keeping
81
+ # -I keeps the fetched copy winning.
82
+ include(CheckCXXCompilerFlag)
83
+ foreach(_w deprecated-enum-float-conversion missing-field-initializers deprecated-declarations)
84
+ check_cxx_compiler_flag("-Wno-${_w}" OSCAD_HAS_WNO_${_w})
85
+ endforeach()
86
+
87
+ if(OSCAD_HAS_WNO_deprecated-enum-float-conversion)
88
+ # <boost/polygon/voronoi.hpp> does floating-point arithmetic on an
89
+ # unnamed enum, deprecated in C++20.
90
+ set_source_files_properties(builtins/roof.cpp PROPERTIES
91
+ COMPILE_OPTIONS "-Wno-deprecated-enum-float-conversion")
92
+ endif()
93
+
94
+ # zip_stored.cpp defines STB_IMAGE_WRITE_IMPLEMENTATION, so stb's C compiles
95
+ # as ours: `= { 0 }` partial initializers, and one sprintf().
96
+ set(_stb_quiet "")
97
+ if(OSCAD_HAS_WNO_missing-field-initializers)
98
+ list(APPEND _stb_quiet "-Wno-missing-field-initializers")
99
+ endif()
100
+ if(OSCAD_HAS_WNO_deprecated-declarations)
101
+ list(APPEND _stb_quiet "-Wno-deprecated-declarations")
102
+ endif()
103
+ if(_stb_quiet)
104
+ set_source_files_properties(zip_stored.cpp PROPERTIES COMPILE_OPTIONS "${_stb_quiet}")
105
+ endif()
@@ -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,
@@ -837,38 +837,6 @@ Value builtinFontmetrics(Evaluator& ev, const CallArgs& args) {
837
837
  });
838
838
  }
839
839
 
840
- // Take only scalar numbers, mapped to how many leading positional
841
- // arguments must each actually BE a number. Anything else there (undef, a
842
- // string, a list, a bool, an object) is a type error yielding undef, and
843
- // so is supplying fewer arguments than the arity -- matching the
844
- // reference, which rejects the call outright rather than coercing.
845
- //
846
- // This has to be checked up front rather than left to each case below:
847
- // toDoubleLenient() silently turns undef/string/list into 0.0, which
848
- // would make `cos(misspelled_var)` quietly 1 instead of undef, and a
849
- // typo'd variable then produces plausible-looking geometry instead of an
850
- // obvious failure. Arity is deliberately a floor, not an exact match --
851
- // the reference also rejects EXTRA arguments, but tightening that too
852
- // risks breaking working library code for no correctness gain here.
853
- const std::unordered_map<std::string, size_t>& scalarNumericArity() {
854
- static const std::unordered_map<std::string, size_t> arity = {
855
- {"abs", 1}, {"sign", 1}, {"ceil", 1}, {"floor", 1}, {"round", 1}, {"sqrt", 1},
856
- {"ln", 1}, {"log", 1}, {"exp", 1}, {"sin", 1}, {"cos", 1}, {"tan", 1},
857
- {"asin", 1}, {"acos", 1}, {"atan", 1}, {"atan2", 2}, {"pow", 2}, {"rands", 3},
858
- };
859
- return arity;
860
- }
861
-
862
- // Numeric functions that legitimately take a LIST argument, so the
863
- // all-arguments-must-be-numbers rule above can't apply -- each validates
864
- // its own operands, and only needs the bool case caught here (Value's
865
- // bool and double are distinct alternatives, but the reference rejects
866
- // `max(true, 1)` as a type error rather than treating true as 1).
867
- const std::unordered_set<std::string>& numericOnlyNames() {
868
- static const std::unordered_set<std::string> names = {"max", "min", "norm", "cross"};
869
- return names;
870
- }
871
-
872
840
  } // namespace
873
841
 
874
842
  bool isBuiltinFunctionName(const std::string& name) {
@@ -1043,6 +1011,7 @@ const std::unordered_map<std::string, double>& featureLevels() {
1043
1011
  {"sphere-styles", 1.0}, // sphere(style=)
1044
1012
  {"export-name", 1.0}, // $export_name
1045
1013
  {"simplify-op", 1.0}, // simplify()
1014
+ {"mesh-repair", 1.0}, // mesh_repair(), and import(tolerance=)
1046
1015
  {"expr-import", 1.0}, // import() in expression position
1047
1016
  {"object-function", 1.0}, // object(), unconditional here
1048
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());
@@ -11,7 +11,10 @@
11
11
  #include <cstdint>
12
12
  #include <map>
13
13
  #include <numbers>
14
+ #include <optional>
14
15
  #include <set>
16
+ #include <string>
17
+ #include <utility>
15
18
 
16
19
  namespace oscadeval {
17
20
 
@@ -42,8 +45,18 @@ bool isDrawableFailure(const manifold::Manifold& body, const M& mesh) {
42
45
  [](float v) { return std::isfinite(v); });
43
46
  }
44
47
 
48
+ struct BoundaryEdges {
49
+ size_t count = 0;
50
+ // Vertex indices of one representative open edge, or nullopt when the
51
+ // mesh is closed. A count alone is enough on a hand-written face list,
52
+ // where you can just read it; it is useless on generated geometry --
53
+ // three bad edges in a few thousand triangles cannot be found by eye,
54
+ // which is what happened in BelfrySCAD #521 / issue #187.
55
+ std::optional<std::pair<uint32_t, uint32_t>> first;
56
+ };
57
+
45
58
  template <typename M>
46
- size_t countBoundaryEdges(const M& mesh) {
59
+ BoundaryEdges findBoundaryEdges(const M& mesh) {
47
60
  std::map<std::pair<uint32_t, uint32_t>, int> uses;
48
61
  for (size_t i = 0; i + 2 < mesh.triVerts.size(); i += 3) {
49
62
  const uint32_t v[3] = {static_cast<uint32_t>(mesh.triVerts[i]),
@@ -54,11 +67,34 @@ size_t countBoundaryEdges(const M& mesh) {
54
67
  ++uses[{std::min(a, b), std::max(a, b)}];
55
68
  }
56
69
  }
57
- size_t open = 0;
70
+ BoundaryEdges out;
71
+ // std::map iterates in vertex-index order, which is arbitrary from the
72
+ // author's point of view but DETERMINISTIC: the same mesh names the same
73
+ // edge on every run. That matters more than which edge gets picked --
74
+ // an identifier that moves between renders is worse than no identifier.
75
+ // A cluster of open edges is nearly always one hole, so the first is
76
+ // usually representative of the rest.
58
77
  for (const auto& [edge, count] : uses) {
59
- if (count % 2 != 0) ++open;
78
+ if (count % 2 == 0) continue;
79
+ ++out.count;
80
+ if (!out.first) out.first = edge;
60
81
  }
61
- return open;
82
+ return out;
83
+ }
84
+
85
+ // "[1, 2.5, 0] - [1, 3, 0]" for one edge, using the same number formatting
86
+ // echo() uses, so a coordinate can be pasted straight back into a script.
87
+ template <typename M>
88
+ std::string describeEdge(const M& mesh, std::pair<uint32_t, uint32_t> edge) {
89
+ const size_t stride = mesh.numProp > 0 ? static_cast<size_t>(mesh.numProp) : 3;
90
+ auto point = [&](uint32_t v) {
91
+ const size_t base = static_cast<size_t>(v) * stride;
92
+ if (base + 2 >= mesh.vertProperties.size()) return std::string("?");
93
+ return "[" + formatNumber(static_cast<double>(mesh.vertProperties[base])) + ", " +
94
+ formatNumber(static_cast<double>(mesh.vertProperties[base + 1])) + ", " +
95
+ formatNumber(static_cast<double>(mesh.vertProperties[base + 2])) + "]";
96
+ };
97
+ return point(edge.first) + " - " + point(edge.second);
62
98
  }
63
99
  } // namespace
64
100
 
@@ -810,8 +846,23 @@ std::vector<ColoredBody> generatePolyhedron(Evaluator& ev, const CSGParams& para
810
846
  // self-intersections builds perfectly well, and only an OPEN one
811
847
  // reaches this branch. The count points straight at the problem.
812
848
  if (!ev.insideHull) {
813
- ev.warn("polyhedron: mesh is not closed -- " + std::to_string(countBoundaryEdges(mesh)) +
814
- " boundary edge(s); drawing it as a surface. hull() can still "
849
+ const BoundaryEdges open = findBoundaryEdges(mesh);
850
+ // Name one of them. Where the hole IS is the only part of this
851
+ // message an author can act on; the count alone just says to go
852
+ // looking, across the whole mesh.
853
+ const std::string where =
854
+ open.first ? ", first at " + describeEdge(mesh, *open.first) : std::string();
855
+ ev.warn("polyhedron: mesh is not closed -- " + std::to_string(open.count) +
856
+ " boundary edge(s)" + where +
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 "
815
866
  "use its points, but it cannot take part in union/difference/intersection",
816
867
  &node.position());
817
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++
@@ -621,18 +733,6 @@ double bandDistance(double v, double lo, double hi, bool invert) {
621
733
  return invert ? -d : d;
622
734
  }
623
735
 
624
- std::optional<std::array<double, 3>> readVec3(const Value& v) {
625
- const ListPtr* l = std::get_if<ListPtr>(&v);
626
- if (!l || !*l || (*l)->items.size() != 3) return std::nullopt;
627
- std::array<double, 3> out{};
628
- for (size_t i = 0; i < 3; ++i) {
629
- const double* d = std::get_if<double>(&(*l)->items[i]);
630
- if (!d || !std::isfinite(*d)) return std::nullopt;
631
- out[i] = *d;
632
- }
633
- return out;
634
- }
635
-
636
736
  } // namespace
637
737
 
638
738
  // -- 2D: marching squares --------------------------------------------------
@@ -58,9 +58,16 @@ std::vector<ColoredBody> Evaluator::generateTreeImpl(const std::vector<CSGNode*>
58
58
  // skipped. See CSGNode::keyHasClosure.
59
59
  if (node.keyHasClosure) key.reset();
60
60
  }
61
- std::optional<std::vector<ColoredBody>> cached = key ? manifoldCache_->get(*key) : std::nullopt;
61
+ std::optional<CachedSubtree> cached = key ? manifoldCache_->get(*key) : std::nullopt;
62
62
  if (cached) {
63
- node.bodies = std::move(*cached);
63
+ node.bodies = std::move(cached->bodies);
64
+ // Replay what generating this subtree said the first time. A hit
65
+ // runs none of it again, so without this the diagnostics simply
66
+ // disappear on every render after the first -- which is how a
67
+ // permanently-open polyhedron came to look like an intermittent
68
+ // one (BelfrySCAD #521). Goes through emitWarning so an enclosing
69
+ // capture records them too, exactly as a fresh generate would.
70
+ for (const std::string& w : cached->warnings) emitWarning(w);
64
71
  // Skipped entirely while measuring: restampCachedIds writes
65
72
  // idToNode/idToColor, and geometry that is about to be discarded
66
73
  // has no click-to-source identity worth recording. See
@@ -83,6 +90,23 @@ std::vector<ColoredBody> Evaluator::generateTreeImpl(const std::vector<CSGNode*>
83
90
  // inside this hull.
84
91
  const bool savedInsideHull = insideHull;
85
92
  if (node.isBuiltin && node.kind == "hull") insideHull = true;
93
+ // Collect everything this subtree warns about, so put() below can
94
+ // store it with the bodies. Only worth doing when the result is
95
+ // actually cacheable -- an uncacheable node (rands() taint, a
96
+ // closure in the key) regenerates every time and warns every time
97
+ // by itself.
98
+ std::vector<std::string> subtreeWarnings;
99
+ // RAII, unlike the plain save/restore insideHull and
100
+ // generateWarnEntry use above: those restore a value, this one
101
+ // restores a POINTER to a stack vector. An EvalError thrown out
102
+ // of a GenerateFn would otherwise leave warnCapture dangling at
103
+ // a destroyed vector for any warning raised afterwards.
104
+ struct CaptureGuard {
105
+ Evaluator& ev;
106
+ std::vector<std::string>* saved;
107
+ ~CaptureGuard() { ev.warnCapture = saved; }
108
+ } captureGuard{*this, warnCapture};
109
+ if (key) warnCapture = &subtreeWarnings;
86
110
  generateTreeImpl(childPtrs);
87
111
  insideHull = savedInsideHull;
88
112
 
@@ -111,12 +135,18 @@ std::vector<ColoredBody> Evaluator::generateTreeImpl(const std::vector<CSGNode*>
111
135
  node.bodies = flattenCsgTree(node.children);
112
136
  }
113
137
  if (key) {
138
+ warnCapture = captureGuard.saved;
139
+ // Merge inward-out: an ancestor still capturing must end up
140
+ // holding this subtree's lines too, since a hit at ITS level
141
+ // skips everything down here.
142
+ if (warnCapture != nullptr)
143
+ warnCapture->insert(warnCapture->end(), subtreeWarnings.begin(), subtreeWarnings.end());
114
144
  // The geometry itself is still cached while measuring --
115
145
  // cacheKey is content-addressed, so the real render can
116
146
  // legitimately reuse it. Only the PRODUCER attribution is
117
147
  // suppressed: a node that gets discarded must never be
118
148
  // recorded as the origin of geometry a later render draws.
119
- manifoldCache_->put(*key, node.bodies);
149
+ manifoldCache_->put(*key, node.bodies, std::move(subtreeWarnings));
120
150
  if (!measuring_) cacheProducer_[*key] = node.node;
121
151
  }
122
152
  // The children's bodies have been consumed (by the dispatch above,