openscad-cpp-evaluator 1.52.0__tar.gz → 1.52.1__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 (244) hide show
  1. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/CLAUDE.md +37 -4
  2. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/offset3d.cpp +282 -41
  5. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_offset3d.cpp +48 -0
  6. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/.github/workflows/ci.yml +0 -0
  7. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/.github/workflows/wheels.yml +0 -0
  8. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/.gitignore +0 -0
  9. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/.gitmodules +0 -0
  10. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/CMakeLists.txt +0 -0
  11. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/LICENSE +0 -0
  12. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/README.md +0 -0
  13. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/bindings/CMakeLists.txt +0 -0
  14. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/bindings/ast_to_py.cpp +0 -0
  15. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/bindings/module.cpp +0 -0
  16. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/cmake/embed_font.cmake +0 -0
  17. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/cmake/patch_manifold.cmake +0 -0
  18. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/examples/CMakeLists.txt +0 -0
  19. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/examples/eval_perf_benchmark.cpp +0 -0
  20. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/examples/manifold_cache_reuse.cpp +0 -0
  21. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/examples/minimal_debugger.cpp +0 -0
  22. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  23. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/.gitignore +0 -0
  24. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  25. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  26. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/LICENSE +0 -0
  27. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/README.md +0 -0
  28. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  29. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  30. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  31. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  32. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  33. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  34. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  35. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  36. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  37. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  38. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  39. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  40. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  41. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  42. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  43. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  44. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/.DS_Store +0 -0
  45. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  46. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/api.cpp +0 -0
  47. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  48. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  49. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  50. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  51. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  52. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  53. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  54. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  55. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  56. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  57. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  58. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  59. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  60. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  61. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  62. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  63. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/position.cpp +0 -0
  64. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  65. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  66. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  67. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  68. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  69. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  70. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  71. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  72. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  73. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_concurrent_parse.cpp +0 -0
  74. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  75. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  76. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  77. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  78. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  79. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  80. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  81. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  82. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  83. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  84. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_profile_time.cpp +0 -0
  85. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  86. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  87. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  88. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  89. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  90. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  91. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  92. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  93. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  94. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  95. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  96. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  97. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  98. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  99. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  100. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  101. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  102. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  103. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  104. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  105. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  106. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  107. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  108. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  109. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  110. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  111. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  112. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/export.hpp +0 -0
  113. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  114. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  115. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  116. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  117. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  118. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  119. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  120. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  121. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  122. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/offset3d.hpp +0 -0
  123. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  124. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  125. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/reassign.hpp +0 -0
  126. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  127. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  128. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  129. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  130. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  131. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/value.hpp +0 -0
  132. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  133. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/python/openscad_cpp_evaluator/__init__.py +0 -0
  134. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/resources/fonts/LICENSE +0 -0
  135. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  136. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  137. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  138. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  139. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/CMakeLists.txt +0 -0
  140. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/alpha_wrap.cpp +0 -0
  141. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/booleans.cpp +0 -0
  142. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/builtins.hpp +0 -0
  143. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/call_args.cpp +0 -0
  144. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/color.cpp +0 -0
  145. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/control.cpp +0 -0
  146. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/dxf_dim.cpp +0 -0
  147. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/export_as.cpp +0 -0
  148. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/extrude.cpp +0 -0
  149. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/function_builtins.cpp +0 -0
  150. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/import.cpp +0 -0
  151. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/modifiers.cpp +0 -0
  152. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/primitives_2d.cpp +0 -0
  153. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/primitives_3d.cpp +0 -0
  154. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/profile_time.cpp +0 -0
  155. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/registry.cpp +0 -0
  156. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/roof.cpp +0 -0
  157. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/segments.cpp +0 -0
  158. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/surface.cpp +0 -0
  159. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/text.cpp +0 -0
  160. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/topology.cpp +0 -0
  161. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/builtins/transforms.cpp +0 -0
  162. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/bytecode_compiler.cpp +0 -0
  163. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/bytecode_vm.cpp +0 -0
  164. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/colored_body.cpp +0 -0
  165. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/coverage.cpp +0 -0
  166. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/csg_generate.cpp +0 -0
  167. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/csg_resolve.cpp +0 -0
  168. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/css_colors.cpp +0 -0
  169. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/debug/debug_repl.cpp +0 -0
  170. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/debug_profile.cpp +0 -0
  171. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/eval_context.cpp +0 -0
  172. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/eval_error.cpp +0 -0
  173. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/eval_use.cpp +0 -0
  174. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/export.cpp +0 -0
  175. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/expr_eval.cpp +0 -0
  176. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/format_closure.cpp +0 -0
  177. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/import/dxf_import.cpp +0 -0
  178. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/import/mesh_import.cpp +0 -0
  179. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/import/scene_import.cpp +0 -0
  180. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/import/surface_load.cpp +0 -0
  181. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/import/svg_import.cpp +0 -0
  182. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/manifold_cache.cpp +0 -0
  183. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/measure_geometry.cpp +0 -0
  184. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/mesh_check.cpp +0 -0
  185. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/stmt_eval.cpp +0 -0
  186. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/text/font_match.cpp +0 -0
  187. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/text/freetype_font_provider.cpp +0 -0
  188. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/text/text_metrics.cpp +0 -0
  189. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/user_calls.cpp +0 -0
  190. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/utf8.cpp +0 -0
  191. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/value.cpp +0 -0
  192. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/xkcd_colors.cpp +0 -0
  193. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/xkcd_colors.hpp +0 -0
  194. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/src/zip_stored.cpp +0 -0
  195. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/CMakeLists.txt +0 -0
  196. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_alpha_wrap.cpp +0 -0
  197. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_booleans.cpp +0 -0
  198. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_bytecode_compiler.cpp +0 -0
  199. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_children_separate.cpp +0 -0
  200. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_cli.cpp +0 -0
  201. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_control_flow.cpp +0 -0
  202. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_coverage.cpp +0 -0
  203. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_csg_tree.cpp +0 -0
  204. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_debug_hooks.cpp +0 -0
  205. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_discretizer.cpp +0 -0
  206. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_dxf_svg_import.cpp +0 -0
  207. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_eval_context.cpp +0 -0
  208. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_export.cpp +0 -0
  209. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_export_as.cpp +0 -0
  210. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_expr_eval.cpp +0 -0
  211. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_extrude_roof.cpp +0 -0
  212. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_feature_detection.cpp +0 -0
  213. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_function_builtins.cpp +0 -0
  214. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_helpers.hpp +0 -0
  215. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_import_export.cpp +0 -0
  216. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_include_cache.cpp +0 -0
  217. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_manifold_cache.cpp +0 -0
  218. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_mesh_check.cpp +0 -0
  219. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_multi_color_merge.cpp +0 -0
  220. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_openscad_parity.cpp +0 -0
  221. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_primitives.cpp +0 -0
  222. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_profile_time.cpp +0 -0
  223. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_profiling.cpp +0 -0
  224. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_python_bindings.py +0 -0
  225. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_render_expr.cpp +0 -0
  226. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_scope_trail.cpp +0 -0
  227. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_scoping.cpp +0 -0
  228. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_surface.cpp +0 -0
  229. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_tail_calls.cpp +0 -0
  230. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_text.cpp +0 -0
  231. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_transforms.cpp +0 -0
  232. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_undef_args.cpp +0 -0
  233. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_unexpected_args.cpp +0 -0
  234. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_use.cpp +0 -0
  235. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_value.cpp +0 -0
  236. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_viewport_params.cpp +0 -0
  237. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tests/test_zip_stored.cpp +0 -0
  238. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/third_party/predicates/predicates.c +0 -0
  239. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/third_party/predicates/predicates.h +0 -0
  240. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tools/cli/CMakeLists.txt +0 -0
  241. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tools/cli/cli_lib.cpp +0 -0
  242. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tools/cli/cli_lib.hpp +0 -0
  243. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/tools/cli/main.cpp +0 -0
  244. {openscad_cpp_evaluator-1.52.0 → openscad_cpp_evaluator-1.52.1}/uv.lock +0 -0
@@ -2066,6 +2066,9 @@ point verified against the hull's planes). Mitered pieces are trimmed by the shi
2066
2066
  of the faces at their end vertices -- the ends of the whole straight edge (`chainEnds`), not of the
2067
2067
  triangle edge, or `refine()`'s midpoints left a 0.6 spike -- and by far faces only within their
2068
2068
  own convex patch (faces joined through convex or flat edges), which keeps separate parts apart.
2069
+ A straight convex edge made of several triangle edges gets **one** wedge: refine()'s halves each
2070
+ had their own, which met face to face at the midpoint and left a zero-width crack (genus 1 for 0),
2071
+ and Manifold later collapsed it with 0.27 of the volume.
2069
2072
 
2070
2073
  **Robustness rules, each from a failure:**
2071
2074
  - Pieces start `e = min(d, max(1e-5·d, 1e-9·scale))` behind the surface, so none shares a plane
@@ -2097,9 +2100,39 @@ own convex patch (faces joined through convex or flat edges), which keeps separa
2097
2100
  out.
2098
2101
  - `|d|` below `1e-7` of the part's size returns the input: double precision cannot place it.
2099
2102
 
2100
- **Known limits.** Exact-contact chamfer cases (two faces meeting at precisely the distance where a
2101
- chamfer closes) can come out with the wrong genus, 4 of the 96 in the review suite. Speed is about
2102
- 2 s for a 32-segment sphere's `delta=1` (pieces built single-threaded 1.1 s, union 0.85 s, lost
2103
- check 0.4 s), so it is a tool for parts, not for dense scans. `OFFSET3D_TIMING=1` prints each phase's
2103
+ **Exact contact.** Where parts meet at precisely the offset distance, every contact joins (the
2104
+ growth by `1e-9·scale` above), so the result is the one just past contact. Do not check that
2105
+ against Manifold's own `MinkowskiSum`/`MinkowskiDifference` at the same distance: there it joins
2106
+ only some of the contacts (two meeting in two places came out genus 0, not 1) or leaves a
2107
+ zero-volume shell. Compare topology against the reference at `d + 1e-7` instead.
2108
+
2109
+ **Speed.**
2110
+ - Pieces are planned serially, since the wedge bookkeeping feeds the caps, and built as jobs on
2111
+ every core (`parallelFor`, plain `std::thread`s; Manifold's TBB nests inside them).
2112
+ - A convex flat face gets one slab, not one per triangle.
2113
+ - The lost check runs its colours in parallel.
2114
+ - **Outward `delta=` of a convex solid skips the union.** Its offset is convex, so it is the hull
2115
+ of the pieces. Only the corners go into the hull: vertices on three or more shifted planes
2116
+ whose normals span 3D. Manifold's hull of every piece vertex (11k-24k points) came out with two
2117
+ shells. The hull is used only if it has one shell, lies inside every shifted plane, and contains
2118
+ every piece vertex; otherwise the union runs.
2119
+ - Not for `chamfer=`: a convex solid's chamfered offset is not always convex (the hull was up to
2120
+ 2% larger).
2121
+ - Union chunking (parallel sub-unions) was tried and gained nothing: `BatchBoolean` already runs
2122
+ in parallel.
2123
+
2124
+ Against 1.52.0, same machine and load (load average 7-10):
2125
+
2126
+ | Case | 1.52.0 | Now |
2127
+ |---|---|---|
2128
+ | sphere(10,32) `delta=1` | 2.2 s | 0.42 s |
2129
+ | cone256 `delta=1` | 5.1 s | 0.28 s |
2130
+ | sphere(5,48) `delta=1` | 4.9 s | 1.25 s |
2131
+ | sphere(10,32) `r=-1` | 1.57 s | 0.90 s |
2132
+ | sphere(10,32) `chamfer=1` | 0.74 s | 0.59 s |
2133
+ | sphere(10,32) `delta=-1` | 0.38 s | 0.36 s |
2134
+
2135
+ The union of a non-convex part's pieces is now the main cost. It is a tool for parts, not for
2136
+ dense scans. `OFFSET3D_TIMING=1` prints each phase's
2104
2137
  wall time to stderr. `_offset3d(verts, tris, d, mode, segs)` in the Python facade is the test hook
2105
2138
  the prototype's suites run against; not public API. Tests: `tests/test_offset3d.cpp`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.52.0
3
+ Version: 1.52.1
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT AND OFL-1.1
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.52.0"
7
+ version = "1.52.1"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -28,6 +28,7 @@
28
28
  #include <cstdlib>
29
29
  #include <string>
30
30
  #include <array>
31
+ #include <atomic>
31
32
  #include <cmath>
32
33
  #include <functional>
33
34
  #include <limits>
@@ -35,6 +36,7 @@
35
36
  #include <numeric>
36
37
  #include <optional>
37
38
  #include <set>
39
+ #include <thread>
38
40
  #include <unordered_map>
39
41
  #include <vector>
40
42
 
@@ -352,7 +354,26 @@ struct Edge {
352
354
  std::vector<int> ts;
353
355
  };
354
356
 
357
+ // fn(0..n-1) on every core. ponytail: one thread per core over an atomic
358
+ // counter; Manifold's own TBB work nests inside, which TBB handles.
359
+ void parallelFor(size_t n, const std::function<void(size_t)>& fn) {
360
+ std::atomic<size_t> next{0};
361
+ auto worker = [&] {
362
+ for (size_t i; (i = next++) < n;) fn(i);
363
+ };
364
+ const unsigned k = unsigned(std::min<size_t>(std::max(1u, std::thread::hardware_concurrency()), n));
365
+ std::vector<std::thread> pool;
366
+ for (unsigned t = 1; t < k; ++t) pool.emplace_back(worker);
367
+ worker();
368
+ for (std::thread& t : pool) t.join();
369
+ }
370
+
355
371
  class Builder {
372
+ // Each piece is a job, run in parallel by build(): the planning that
373
+ // decides what to build (and fills the wedge bookkeeping the caps read)
374
+ // stays serial, the hulls and trims run on every core.
375
+ using Jobs = std::vector<std::function<Manifold()>>;
376
+
356
377
  public:
357
378
  Builder(const std::vector<vec3>& V, std::vector<std::array<int, 3>> T, double d, Offset3DMode mode, int segs, bool flip)
358
379
  : V_(V), T_(std::move(T)), d_(d), mode_(mode), ball_(sphereDirs(segs)) {
@@ -379,7 +400,21 @@ public:
379
400
  }
380
401
 
381
402
  std::vector<Manifold> build() {
382
- std::vector<Manifold> out;
403
+ Jobs out;
404
+ plan(out);
405
+ std::vector<Manifold> made(out.size());
406
+ parallelFor(out.size(), [&](size_t i) {
407
+ const Manifold m = out[i]();
408
+ if (!m.IsEmpty()) made[i] = bare(m);
409
+ });
410
+ std::vector<Manifold> res;
411
+ for (Manifold& m : made)
412
+ if (!m.IsEmpty()) res.push_back(std::move(m));
413
+ return res;
414
+ }
415
+
416
+ private:
417
+ void plan(Jobs& out) {
383
418
  if (mode_ == Offset3DMode::Round) {
384
419
  // Each triangle swept by the one faceted ball: exactly the
385
420
  // Minkowski sum with it.
@@ -388,19 +423,17 @@ public:
388
423
  for (int k = 0; k < 3; ++k) pts.push_back(V_[T_[t][k]] - e_ * N_[t]);
389
424
  for (int k = 0; k < 3; ++k)
390
425
  for (const vec3& b : ball_) pts.push_back(V_[T_[t][k]] + d_ * b);
391
- push(out, hullChecked(pts));
426
+ push(out, [pts] { return hullChecked(pts); });
392
427
  }
393
- return out;
428
+ return;
394
429
  }
395
430
  indexEdges();
396
- slabs(out);
397
431
  classify();
432
+ slabs(out);
398
433
  wedges(out);
399
434
  caps(out);
400
- return out;
401
435
  }
402
436
 
403
- private:
404
437
  const std::vector<vec3>& V_;
405
438
  std::vector<std::array<int, 3>> T_;
406
439
  double d_;
@@ -424,9 +457,7 @@ private:
424
457
  std::map<int, std::vector<int>> members_;
425
458
  std::map<int, bool> gone_;
426
459
 
427
- static void push(std::vector<Manifold>& out, Manifold m) {
428
- if (!m.IsEmpty()) out.push_back(bare(m));
429
- }
460
+ static void push(Jobs& out, std::function<Manifold()> job) { out.push_back(std::move(job)); }
430
461
 
431
462
  uint64_t key(int a, int b) const {
432
463
  return (uint64_t(std::min(a, b)) << 32) | uint64_t(std::max(a, b));
@@ -470,8 +501,82 @@ private:
470
501
  // Past a concave edge the sliver lands inside A outward, outside A
471
502
  // inward, and past a flat one inside the neighbour's slab: within d of the
472
503
  // surface every time. Convex edges have their wedges' strips.
473
- void slabs(std::vector<Manifold>& out) {
504
+ // The boundary of a convex flat face, or nothing if the face is not convex
505
+ // (or has a hole): every face vertex on the inner side of every boundary
506
+ // edge. q gets the face's vertices, plus each non-convex boundary edge's
507
+ // ends pushed e outward, as the triangle slabs below widen them.
508
+ bool convexFace(int g, const std::vector<int>& ts, std::vector<vec3>& q) const {
509
+ const vec3 n = N_[ts[0]];
510
+ std::set<int> vs;
511
+ for (int t : ts)
512
+ for (int v : T_[t]) vs.insert(v);
513
+ std::vector<vec3> wide;
514
+ for (int t : ts)
515
+ for (int i = 0; i < 3; ++i) {
516
+ const int a = T_[t][i], b = T_[t][(i + 1) % 3];
517
+ const int u = across(t, a, b);
518
+ if (flat_[u] == g) continue;
519
+ const vec3 in = la::cross(n, V_[b] - V_[a]); // left of a->b: the face's side
520
+ const double tol = 1e-9 * scale_ * la::length(in);
521
+ for (int v : vs)
522
+ if (la::dot(in, V_[v] - V_[a]) < -tol) return false;
523
+ if (isConvex(t, u, a, b)) continue;
524
+ const vec3 k = -la::normalize(in);
525
+ wide.push_back(V_[a] + e_ * k);
526
+ wide.push_back(V_[b] + e_ * k);
527
+ }
528
+ std::vector<vec3> all;
529
+ for (int v : vs) all.push_back(V_[v]);
530
+ all.insert(all.end(), wide.begin(), wide.end());
531
+ q = corners2D(all, n);
532
+ return q.size() >= 3;
533
+ }
534
+
535
+ // The strict corners of coplanar points (normal n): a 2D hull in the
536
+ // plane, collinear points dropped. refine() leaves midpoints along every
537
+ // edge; Manifold's hull kept them as zero-area slivers (a 422-triangle
538
+ // slab), and the union with them lost 0.27 of t41 seed 18 once moved back.
539
+ std::vector<vec3> corners2D(const std::vector<vec3>& P, const vec3& n) const {
540
+ const vec3 u = la::normalize(std::fabs(n.x) < 0.9 ? la::cross(n, vec3(1, 0, 0)) : la::cross(n, vec3(0, 1, 0)));
541
+ const vec3 w = la::cross(n, u);
542
+ std::vector<std::pair<std::array<double, 2>, size_t>> pts;
543
+ for (size_t i = 0; i < P.size(); ++i) pts.push_back({{la::dot(P[i], u), la::dot(P[i], w)}, i});
544
+ std::sort(pts.begin(), pts.end());
545
+ const double tol = 1e-12 * scale_ * scale_;
546
+ auto cross = [](const std::array<double, 2>& o, const std::array<double, 2>& a, const std::array<double, 2>& b) {
547
+ return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
548
+ };
549
+ std::vector<size_t> h(2 * pts.size());
550
+ size_t k = 0;
551
+ for (size_t i = 0; i < pts.size(); ++i) { // Andrew's monotone chain
552
+ while (k >= 2 && cross(pts[h[k - 2]].first, pts[h[k - 1]].first, pts[i].first) <= tol) --k;
553
+ h[k++] = i;
554
+ }
555
+ for (size_t i = pts.size() - 1, t = k + 1; i-- > 0;) {
556
+ while (k >= t && cross(pts[h[k - 2]].first, pts[h[k - 1]].first, pts[i].first) <= tol) --k;
557
+ h[k++] = i;
558
+ }
559
+ std::vector<vec3> out;
560
+ for (size_t i = 0; i + 1 < k; ++i) out.push_back(P[pts[h[i]].second]);
561
+ return out;
562
+ }
563
+
564
+ void slabs(Jobs& out) {
565
+ // One slab per convex flat face: a fan of 254 slabs meeting at one
566
+ // vertex (a cylinder's end) made the union several times slower.
567
+ std::vector<char> done(T_.size(), 0);
568
+ for (const auto& [g, ts] : members_) {
569
+ std::vector<vec3> q;
570
+ if (ts.size() < 2 || !convexFace(g, ts, q)) continue;
571
+ for (int t : ts) done[t] = 1;
572
+ const vec3 n = N_[ts[0]];
573
+ std::vector<vec3> pts;
574
+ for (const vec3& p : q) pts.push_back(p - e_ * n);
575
+ for (const vec3& p : q) pts.push_back(p + d_ * n);
576
+ push(out, [pts] { return hullChecked(pts); });
577
+ }
474
578
  for (size_t t = 0; t < T_.size(); ++t) {
579
+ if (done[t]) continue;
475
580
  const vec3 n = N_[t];
476
581
  std::vector<vec3> q;
477
582
  for (int k = 0; k < 3; ++k) q.push_back(V_[T_[t][k]]);
@@ -487,7 +592,7 @@ private:
487
592
  std::vector<vec3> pts;
488
593
  for (const vec3& p : q) pts.push_back(p - e_ * n);
489
594
  for (const vec3& p : q) pts.push_back(p + d_ * n);
490
- push(out, hullChecked(pts));
595
+ push(out, [pts] { return hullChecked(pts); });
491
596
  }
492
597
  }
493
598
 
@@ -690,7 +795,12 @@ private:
690
795
  return {walk(e.b, e.a), walk(e.a, e.b)};
691
796
  }
692
797
 
693
- void wedges(std::vector<Manifold>& out) {
798
+ // One wedge per straight convex edge, however many triangle edges it is
799
+ // made of: refine() split an edge in two, and the halves' wedges met face
800
+ // to face at the midpoint -- a zero-width crack (genus 1 for 0), which
801
+ // Manifold later collapsed and took 0.27 of t41 seed 18's volume with it.
802
+ void wedges(Jobs& out) {
803
+ std::map<std::pair<int, int>, std::vector<size_t>> chains;
694
804
  for (size_t ei = 0; ei < edges_.size(); ++ei) {
695
805
  const Edge& e = edges_[ei];
696
806
  const int a = e.a, b = e.b, t1 = e.ts[0], t2 = e.ts[1];
@@ -701,38 +811,52 @@ private:
701
811
  convEdgeOther_[a].push_back(b);
702
812
  convEdgeNormals_[b].push_back({n1, n2});
703
813
  convEdgeOther_[b].push_back(a);
704
- const vec3 pa = V_[a], pb = V_[b];
705
- const vec3 c1 = V_[otherVertex(t1, a, b)], c2 = V_[otherVertex(t2, a, b)];
706
- // A sliver of each adjacent slab, so the wedge overlaps both:
707
- // walls that touch exactly leave rounding-thin tunnels.
814
+ const auto [ea, eb] = chainEnds(ei);
815
+ chains[{std::min(ea, eb), std::max(ea, eb)}].push_back(ei);
816
+ }
817
+ for (const auto& [ends, members] : chains) {
818
+ const auto [ea, eb] = ends;
819
+ const Edge& e0 = edges_[members[0]];
820
+ const vec3 n1 = N_[e0.ts[0]], n2 = N_[e0.ts[1]];
821
+ const vec3 pa = V_[ea], pb = V_[eb];
708
822
  std::vector<vec3> pts{pa - e_ * (n1 + n2), pb - e_ * (n1 + n2), pa + d_ * n1, pb + d_ * n1,
709
823
  pa + d_ * n2, pb + d_ * n2};
710
- for (const auto& [c, n] : {std::pair{c1, n1}, std::pair{c2, n2}})
711
- for (const vec3& p : {pa, pb})
712
- for (const vec3& h : {-e_ * n, d_ * n}) pts.push_back(toward(p, c, kLam * d_) + h);
824
+ std::vector<int> tris;
825
+ // A sliver of each adjacent slab, so the wedge overlaps both:
826
+ // walls that touch exactly leave rounding-thin tunnels.
827
+ for (size_t ei : members) {
828
+ const Edge& e = edges_[ei];
829
+ for (int t : e.ts) {
830
+ tris.push_back(t);
831
+ const vec3 c = V_[otherVertex(t, e.a, e.b)], n = N_[t];
832
+ for (const vec3& p : {V_[e.a], V_[e.b]})
833
+ for (const vec3& h : {-e_ * n, d_ * n}) pts.push_back(toward(p, c, kLam * d_) + h);
834
+ }
835
+ }
713
836
  if (mode_ != Offset3DMode::Delta) {
714
- push(out, hullChecked(pts));
837
+ push(out, [pts] { return hullChecked(pts); });
715
838
  continue;
716
839
  }
717
840
  const vec3 m = miter(n1, n2);
718
841
  pts.push_back(pa + d_ * m);
719
842
  pts.push_back(pb + d_ * m);
720
- Manifold w = hullChecked(pts);
721
843
  // The miter must not run past any plane meeting at either end (a
722
844
  // sharp edge's miter line would otherwise overshoot a third face
723
845
  // that tilts over it).
724
- const auto [ea, eb] = chainEnds(ei);
725
- for (const int v : {ea, eb})
726
- if (!concaveAt_[v]) {
727
- std::vector<vec3> ns;
728
- for (int t : vertFaces_[v]) ns.push_back(N_[t]);
729
- w = clipPlanes(w, ns, V_[v], d_);
730
- }
731
- push(out, clipFar(w, {V_[ea], V_[eb]}, pts, reach({t1, t2})));
846
+ push(out, [this, pts, ea, eb, tris] {
847
+ Manifold w = hullChecked(pts);
848
+ for (const int v : {ea, eb})
849
+ if (!concaveAt_[v]) {
850
+ std::vector<vec3> ns;
851
+ for (int t : vertFaces_[v]) ns.push_back(N_[t]);
852
+ w = clipPlanes(w, ns, V_[v], d_);
853
+ }
854
+ return clipFar(w, {V_[ea], V_[eb]}, pts, reach(tris));
855
+ });
732
856
  }
733
857
  }
734
858
 
735
- void caps(std::vector<Manifold>& out) {
859
+ void caps(Jobs& out) {
736
860
  for (size_t v = 0; v < V_.size(); ++v) {
737
861
  const std::vector<int>& faces = vertFaces_[v];
738
862
  if (faces.empty()) continue;
@@ -753,7 +877,7 @@ private:
753
877
  for (int t : faces)
754
878
  for (int c : T_[t])
755
879
  if (c != int(v)) pts.push_back(toward(pv, V_[c], kLam * d_) + d_ * N_[t]);
756
- push(out, hullChecked(pts));
880
+ push(out, [pts] { return hullChecked(pts); });
757
881
  }
758
882
  continue;
759
883
  }
@@ -763,7 +887,7 @@ private:
763
887
  // d of v is in any true mitered offset, so the ball is safe.
764
888
  std::vector<vec3> pts;
765
889
  for (const vec3& b : ball_) pts.push_back(pv + d_ * b);
766
- push(out, hullChecked(pts));
890
+ push(out, [pts] { return hullChecked(pts); });
767
891
  }
768
892
  if (ns.size() < 3) continue; // the edge wedges already meet here
769
893
  std::vector<vec3> pts{pv - e_ * sum};
@@ -777,7 +901,7 @@ private:
777
901
  if (mode_ == Offset3DMode::Delta) pts.push_back(q + d_ * miter(n1, n2));
778
902
  }
779
903
  if (mode_ != Offset3DMode::Delta || concaveAt_[v]) {
780
- push(out, hullChecked(pts));
904
+ push(out, [pts] { return hullChecked(pts); });
781
905
  continue;
782
906
  }
783
907
  for (const vec3& x : cornerPoints(pv, ns, d_)) pts.push_back(x);
@@ -792,6 +916,7 @@ private:
792
916
  } else {
793
917
  for (const auto& nn : convEdgeNormals_[v]) pts.push_back(pv + d_ * miter(nn[0], nn[1]));
794
918
  }
919
+ push(out, [this, pts, pv, fn, faces] {
795
920
  Manifold cap = hullChecked(pts);
796
921
  // The cap is the hull of a few points of the mitered corner, which
797
922
  // need not hold the ball of radius d round v -- though the corner
@@ -812,7 +937,8 @@ private:
812
937
  }
813
938
  }
814
939
  cap = clipPlanes(cap, fn, pv, d_);
815
- push(out, clipFar(cap, {pv}, pts, reach(faces)));
940
+ return clipFar(cap, {pv}, pts, reach(faces));
941
+ });
816
942
  }
817
943
  }
818
944
  };
@@ -914,23 +1040,27 @@ std::vector<size_t> lostPieces(const std::vector<Manifold>& ps, const Manifold&
914
1040
  ncol = std::max(ncol, c + 1);
915
1041
  grid[k].push_back(i);
916
1042
  }
917
- std::set<size_t> lost;
918
- for (int c = 0; c < ncol; ++c) {
1043
+ // Each colour is its own check: all of them at once.
1044
+ std::vector<std::vector<size_t>> lostBy(ncol);
1045
+ parallelFor(size_t(ncol), [&](size_t c) {
919
1046
  std::vector<Manifold> group;
920
1047
  std::vector<size_t> who;
921
1048
  for (size_t i = 0; i < n; ++i)
922
- if (colour[i] == c) { group.push_back(ps[i]); who.push_back(i); }
1049
+ if (colour[i] == int(c)) { group.push_back(ps[i]); who.push_back(i); }
923
1050
  const Manifold G = Manifold::Compose(group);
924
1051
  const Manifold R = wantInside ? G - u : G ^ u;
925
- if (R.IsEmpty()) continue;
1052
+ if (R.IsEmpty()) return;
926
1053
  for (const Manifold& comp : R.Decompose()) {
927
1054
  const double v = std::fabs(comp.Volume()), a = comp.SurfaceArea();
928
1055
  if (!(a > 0) || 2 * v / a <= 10 * e) continue;
929
1056
  const manifold::Box cb = comp.BoundingBox();
930
1057
  for (size_t i : who)
931
- if (la::all(la::lequal(box[i].min, cb.max)) && la::all(la::lequal(cb.min, box[i].max))) lost.insert(i);
1058
+ if (la::all(la::lequal(box[i].min, cb.max)) && la::all(la::lequal(cb.min, box[i].max)))
1059
+ lostBy[c].push_back(i);
932
1060
  }
933
- }
1061
+ });
1062
+ std::set<size_t> lost;
1063
+ for (const auto& l : lostBy) lost.insert(l.begin(), l.end());
934
1064
  return {lost.begin(), lost.end()};
935
1065
  }
936
1066
 
@@ -1030,6 +1160,39 @@ Manifold cleanShells(const Manifold& out, const Manifold& A, const std::function
1030
1160
  return holes.empty() ? res : res - unionPieces(holes);
1031
1161
  }
1032
1162
 
1163
+ // One closed, connected surface with no concave edge: a convex solid (a
1164
+ // locally convex closed surface bounds a convex body).
1165
+ bool convexSolid(const Mesh& mesh) {
1166
+ const size_t nt = mesh.T.size();
1167
+ if (nt < 4) return false;
1168
+ std::vector<vec3> N(nt);
1169
+ for (size_t t = 0; t < nt; ++t) {
1170
+ const auto& f = mesh.T[t];
1171
+ N[t] = la::normalize(la::cross(mesh.V[f[1]] - mesh.V[f[0]], mesh.V[f[2]] - mesh.V[f[0]]));
1172
+ }
1173
+ std::unordered_map<uint64_t, std::vector<int>> edges;
1174
+ for (size_t t = 0; t < nt; ++t)
1175
+ for (int i = 0; i < 3; ++i) {
1176
+ const int a = mesh.T[t][i], b = mesh.T[t][(i + 1) % 3];
1177
+ edges[(uint64_t(std::min(a, b)) << 32) | uint64_t(std::max(a, b))].push_back(int(t));
1178
+ }
1179
+ std::vector<int> root(nt);
1180
+ std::iota(root.begin(), root.end(), 0);
1181
+ std::function<int(int)> find = [&](int i) { return root[i] == i ? i : root[i] = find(root[i]); };
1182
+ const double tol = 1e-9 * std::max(1.0, maxAbs(mesh.V));
1183
+ for (const auto& [k, ts] : edges) {
1184
+ if (ts.size() != 2) return false;
1185
+ const int a = int(k >> 32), t1 = ts[0], t2 = ts[1];
1186
+ root[find(t1)] = find(t2);
1187
+ if (la::dot(N[t1], N[t2]) > kFlat) continue;
1188
+ for (int v : mesh.T[t2])
1189
+ if (la::dot(N[t1], mesh.V[v] - mesh.V[a]) > tol) return false; // concave
1190
+ }
1191
+ for (size_t t = 0; t < nt; ++t)
1192
+ if (find(int(t)) != find(0)) return false;
1193
+ return true;
1194
+ }
1195
+
1033
1196
  Manifold offsetCentered(const Manifold& m, double d, Offset3DMode mode, int segs) {
1034
1197
  const manifold::Box bb = m.BoundingBox();
1035
1198
  const vec3 size = bb.max - bb.min;
@@ -1061,7 +1224,85 @@ Manifold offsetCentered(const Manifold& m, double d, Offset3DMode mode, int segs
1061
1224
  const double mu = 1e-9 * std::max(1.0, maxAbs(mesh.V));
1062
1225
  ps = Builder(mesh.V, mesh.T, std::fabs(d) + mu, mode, segs, d < 0).build();
1063
1226
  timer.lap("pieces");
1064
- tools = unionPieces(ps);
1227
+ if (d > 0 && mode == Offset3DMode::Delta && convexSolid(mesh)) {
1228
+ // The mitered offset of a convex solid is convex, so it is the
1229
+ // hull of the pieces that fill it: one hull, not a union of
1230
+ // hundreds of near-coplanar wedges (a 256-sided cone: 4.4 s).
1231
+ // Not chamfer=: its offset of a convex solid need not be convex.
1232
+ const double tol = 1e-9 * std::max(1.0, maxAbs(mesh.V)), D = d + mu;
1233
+ std::vector<vec3> fn;
1234
+ std::vector<double> fc;
1235
+ for (const auto& f : mesh.T) {
1236
+ const vec3 n = la::normalize(la::cross(mesh.V[f[1]] - mesh.V[f[0]], mesh.V[f[2]] - mesh.V[f[0]]));
1237
+ fn.push_back(n);
1238
+ fc.push_back(la::dot(n, mesh.V[f[0]]) + D);
1239
+ }
1240
+ // The pieces' vertices, near-duplicates (shared corners, ~1e-9 apart)
1241
+ // merged to one exact point each rather than snapped to a grid.
1242
+ std::map<std::array<long long, 3>, vec3> near;
1243
+ const double q = 10 * tol;
1244
+ for (const Manifold& p : ps)
1245
+ for (const vec3& v : meshOf(p).V)
1246
+ near.emplace(std::array<long long, 3>{std::llround(v.x / q), std::llround(v.y / q), std::llround(v.z / q)}, v);
1247
+ // Only the offset's corners go in: a vertex of an intersection of
1248
+ // half-spaces lies on three or more planes whose normals span 3D.
1249
+ // Manifold's hull of every piece vertex (11k-24k of them, most on a
1250
+ // face's plane) came out with two shells, or points 3e-4 outside.
1251
+ std::vector<vec3> all;
1252
+ for (const auto& kv : near) all.push_back(kv.second);
1253
+ std::vector<char> corner(all.size(), 0);
1254
+ parallelFor(all.size(), [&](size_t a) {
1255
+ const vec3 v = all[a];
1256
+ std::vector<vec3> on;
1257
+ bool inside = true;
1258
+ for (size_t j = 0; j < fn.size() && inside; ++j) {
1259
+ const double w = la::dot(fn[j], v) - fc[j];
1260
+ if (w > 10 * tol) inside = false;
1261
+ else if (w > -10 * tol) on.push_back(fn[j]);
1262
+ }
1263
+ if (!inside || on.size() < 3) return;
1264
+ bool span = false;
1265
+ for (size_t i = 1; i < on.size() && !span; ++i) {
1266
+ const vec3 c = la::cross(on[0], on[i]);
1267
+ if (la::length(c) < 1e-9) continue;
1268
+ for (size_t j = i + 1; j < on.size() && !span; ++j)
1269
+ span = std::fabs(la::dot(c, on[j])) > 1e-9;
1270
+ }
1271
+ corner[a] = span;
1272
+ });
1273
+ std::vector<vec3> pts;
1274
+ for (size_t a = 0; a < all.size(); ++a)
1275
+ if (corner[a]) pts.push_back(all[a]);
1276
+ // Trusted only if provably the offset: one shell, inside every
1277
+ // shifted plane (so within the offset), and holding every piece
1278
+ // vertex (so every piece, and their union). Otherwise the union
1279
+ // runs as for any other input.
1280
+ Manifold h = pts.size() >= 4 ? whitenedHull(pts) : Manifold();
1281
+ bool good = !h.IsEmpty() && h.Status() == Manifold::Error::NoError && h.Genus() == 0 &&
1282
+ h.Decompose().size() == 1;
1283
+ if (good) {
1284
+ const Planes pl = planesOf(h);
1285
+ std::atomic<bool> ok{true};
1286
+ parallelFor(all.size(), [&](size_t a) {
1287
+ for (size_t j = 0; ok && j < pl.n.size(); ++j)
1288
+ if (la::dot(all[a], pl.n[j]) > pl.c[j] + 10 * tol) ok = false;
1289
+ });
1290
+ const std::vector<vec3> hv = meshOf(h).V;
1291
+ parallelFor(hv.size(), [&](size_t a) {
1292
+ for (size_t j = 0; ok && j < fn.size(); ++j)
1293
+ if (la::dot(fn[j], hv[a]) > fc[j] + 10 * tol) ok = false;
1294
+ });
1295
+ good = ok;
1296
+ }
1297
+ if (good) {
1298
+ ps.clear(); // nothing for lostPieces() to check: no union ran
1299
+ tools = h;
1300
+ } else {
1301
+ tools = unionPieces(ps);
1302
+ }
1303
+ } else {
1304
+ tools = unionPieces(ps);
1305
+ }
1065
1306
  timer.lap("union");
1066
1307
  out = d > 0 ? m + tools : m - tools;
1067
1308
  out.NumTri();
@@ -63,6 +63,13 @@ TEST(Offset3D, CubeEveryModeBothWays) {
63
63
  EXPECT_GT(r, exact * 0.99);
64
64
  }
65
65
 
66
+ TEST(Offset3D, ConvexMiteredOffsetIsExact) {
67
+ // A convex solid's delta= offset is built as one hull: a 64-sided prism
68
+ // grows to the 64-gon whose apothem is one more, extruded 2 taller.
69
+ const double n = 64, a = 5 * std::cos(M_PI / n) + 1;
70
+ EXPECT_VOL("offset(delta=1) cylinder(h=10, r=5, $fn=64);", 12 * n * a * a * std::tan(M_PI / n));
71
+ }
72
+
66
73
  TEST(Offset3D, ThinPlateVanishesInward) {
67
74
  EXPECT_NEAR(vol("offset(delta=-1) cube([10, 10, 1.5]);"), 0, 1e-9);
68
75
  EXPECT_NEAR(vol("offset(r=-1) cube([10, 10, 1.5]);"), 0, 1e-9);
@@ -107,6 +114,47 @@ TEST(Offset3D, TopologyChangesAreKept) {
107
114
  EXPECT_EQ(solid("offset(delta=-1) " + U).Decompose().size(), 1u);
108
115
  }
109
116
 
117
+ TEST(Offset3D, TouchingAtExactlyTheDistanceJoinsEveryContact) {
118
+ // Grown by 0.5 with chamfers, two parts of this block meet in two places at
119
+ // once: genus -1 (two shells) just short of 0.5, genus 1 just past it. At
120
+ // exactly 0.5 touching counts as joined, at both contacts -- Manifold's own
121
+ // Minkowski sum joins only one of them there (genus 0).
122
+ const std::string P =
123
+ "difference() { difference() { difference() { union() { union() { union() { cube(8);"
124
+ " translate([2, -2, -2]) cube([5, 1, 4]); } translate([3, 3, -2]) cube([5, 4, 6]); }"
125
+ " translate([6, 6, -1]) cube([2, 6, 4]); } translate([2, 7, -1]) cube([5, 6, 6]); }"
126
+ " translate([4, 2, 5]) cube([3, 1, 3]); } translate([2, -1, -2]) cube([2, 6, 6]); }";
127
+ const Manifold o = solid("offset(delta=0.5, chamfer=true) " + P);
128
+ EXPECT_EQ(o.Decompose().size(), 1u);
129
+ EXPECT_EQ(o.Genus(), 1);
130
+ EXPECT_NEAR(o.Volume(), 818.75, 1e-6 * 818.75);
131
+ }
132
+
133
+ TEST(Offset3D, SplitEdgesGiveTheSameOffset) {
134
+ // A 10-cube whose faces are each a 2x2 grid: every edge is two triangle
135
+ // edges and every face eight triangles. Its offset is the plain cube's.
136
+ // (The halves of a split edge once got a wedge each, meeting face to face;
137
+ // on t41 seed 18's refine(2) that left a zero-width crack, genus 1, that
138
+ // cost 0.27 of volume. This cube came out right even then: it guards the
139
+ // one-wedge-per-straight-edge path, it does not reproduce that crack.)
140
+ const std::string grid =
141
+ "polyhedron([[0,0,0],[0,0,5],[0,0,10],[0,5,0],[0,5,5],[0,5,10],[0,10,0],[0,10,5],[0,10,10],[5,0,0"
142
+ "],[5,0,5],[5,0,10],[5,5,0],[5,5,10],[5,10,0],[5,10,5],[5,10,10],[10,0,0],[10,0,5],[10,0,10],[10,"
143
+ "5,0],[10,5,5],[10,5,10],[10,10,0],[10,10,5],[10,10,10]], [[0,3,4],[0,4,1],[1,4,5],[1,5,2],[3,6,7"
144
+ "],[3,7,4],[4,7,8],[4,8,5],[18,21,20],[18,20,17],[19,22,21],[19,21,18],[21,24,23],[21,23,20],[22,"
145
+ "25,24],[22,24,21],[1,10,9],[1,9,0],[2,11,10],[2,10,1],[10,18,17],[10,17,9],[11,19,18],[11,18,10]"
146
+ ",[6,14,15],[6,15,7],[7,15,16],[7,16,8],[14,23,24],[14,24,15],[15,24,25],[15,25,16],[0,9,12],[0,1"
147
+ "2,3],[3,12,14],[3,14,6],[9,17,20],[9,20,12],[12,20,23],[12,23,14],[5,13,11],[5,11,2],[8,16,13],["
148
+ "8,13,5],[13,22,19],[13,19,11],[16,25,22],[16,22,13]]);";
149
+ ASSERT_NEAR(vol(grid), 1000, 1e-9);
150
+ for (const std::string& mode : {"delta=1", "delta=1, chamfer=true", "delta=-1", "delta=-1, chamfer=true"}) {
151
+ const Manifold a = solid("offset(" + mode + ") cube(10);"), b = solid("offset(" + mode + ") " + grid);
152
+ EXPECT_NEAR(b.Volume(), a.Volume(), 1e-6 * a.Volume()) << mode;
153
+ EXPECT_EQ(b.Genus(), 0) << mode;
154
+ EXPECT_EQ(b.Decompose().size(), 1u) << mode;
155
+ }
156
+ }
157
+
110
158
  TEST(Offset3D, FarFromTheOriginIsTheSame) {
111
159
  // The offset runs at the origin: Manifold's union lost 0.35 of this
112
160
  // volume at 1e5 from it.