openscad-cpp-evaluator 1.46.0__tar.gz → 1.47.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 (234) hide show
  1. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/PKG-INFO +1 -1
  2. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/pyproject.toml +1 -1
  3. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/topology.cpp +234 -40
  4. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_booleans.cpp +208 -8
  5. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/uv.lock +1 -1
  6. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/.github/workflows/ci.yml +0 -0
  7. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/.github/workflows/wheels.yml +0 -0
  8. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/.gitignore +0 -0
  9. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/.gitmodules +0 -0
  10. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/CLAUDE.md +0 -0
  11. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/CMakeLists.txt +0 -0
  12. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/LICENSE +0 -0
  13. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/README.md +0 -0
  14. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/bindings/CMakeLists.txt +0 -0
  15. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/bindings/ast_to_py.cpp +0 -0
  16. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/bindings/module.cpp +0 -0
  17. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/cmake/embed_font.cmake +0 -0
  18. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/cmake/patch_manifold.cmake +0 -0
  19. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/examples/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/examples/eval_perf_benchmark.cpp +0 -0
  21. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/examples/manifold_cache_reuse.cpp +0 -0
  22. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/examples/minimal_debugger.cpp +0 -0
  23. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  24. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/.gitignore +0 -0
  25. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  26. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  27. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/LICENSE +0 -0
  28. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/README.md +0 -0
  29. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  30. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  31. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  32. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  33. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  34. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  35. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  36. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  37. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  38. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  39. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  40. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  41. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  42. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  43. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  44. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  45. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/.DS_Store +0 -0
  46. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  47. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  48. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  49. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  50. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  51. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  52. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  53. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  54. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  55. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  56. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  57. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  58. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  59. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  60. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  61. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  62. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  63. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  64. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  65. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  66. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  67. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  68. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  69. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  70. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  71. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  72. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  73. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  74. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_concurrent_parse.cpp +0 -0
  75. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  76. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  77. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  78. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  79. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  80. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  81. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  82. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  83. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  84. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  85. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_profile_time.cpp +0 -0
  86. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  87. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  88. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  89. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  90. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  91. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  92. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  93. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  94. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  95. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  96. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  97. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  98. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  99. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  100. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  101. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  102. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  103. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  104. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  105. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  106. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  107. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  108. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  109. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  110. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  111. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  112. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  113. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  114. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  115. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  116. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  117. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  118. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  119. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  120. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  121. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  122. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  123. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  124. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  125. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  126. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  127. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  128. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  129. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  130. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  131. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  132. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  133. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/resources/fonts/LICENSE +0 -0
  134. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  135. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  136. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  137. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  138. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/CMakeLists.txt +0 -0
  139. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/booleans.cpp +0 -0
  140. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/builtins.hpp +0 -0
  141. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/call_args.cpp +0 -0
  142. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/color.cpp +0 -0
  143. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/control.cpp +0 -0
  144. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/dxf_dim.cpp +0 -0
  145. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/extrude.cpp +0 -0
  146. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/function_builtins.cpp +0 -0
  147. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/import.cpp +0 -0
  148. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/modifiers.cpp +0 -0
  149. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/primitives_2d.cpp +0 -0
  150. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/primitives_3d.cpp +0 -0
  151. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/profile_time.cpp +0 -0
  152. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/registry.cpp +0 -0
  153. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/roof.cpp +0 -0
  154. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/segments.cpp +0 -0
  155. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/surface.cpp +0 -0
  156. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/text.cpp +0 -0
  157. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/builtins/transforms.cpp +0 -0
  158. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/bytecode_compiler.cpp +0 -0
  159. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/bytecode_vm.cpp +0 -0
  160. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/colored_body.cpp +0 -0
  161. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/coverage.cpp +0 -0
  162. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/csg_generate.cpp +0 -0
  163. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/csg_resolve.cpp +0 -0
  164. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/css_colors.cpp +0 -0
  165. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/debug/debug_repl.cpp +0 -0
  166. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/debug_profile.cpp +0 -0
  167. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/eval_context.cpp +0 -0
  168. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/eval_error.cpp +0 -0
  169. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/eval_use.cpp +0 -0
  170. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/export.cpp +0 -0
  171. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/expr_eval.cpp +0 -0
  172. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/format_closure.cpp +0 -0
  173. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/import/dxf_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/import/mesh_import.cpp +0 -0
  175. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/import/scene_import.cpp +0 -0
  176. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/import/surface_load.cpp +0 -0
  177. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/import/svg_import.cpp +0 -0
  178. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/manifold_cache.cpp +0 -0
  179. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/measure_geometry.cpp +0 -0
  180. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/mesh_check.cpp +0 -0
  181. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/stmt_eval.cpp +0 -0
  182. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/text/font_match.cpp +0 -0
  183. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/text/freetype_font_provider.cpp +0 -0
  184. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/text/text_metrics.cpp +0 -0
  185. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/user_calls.cpp +0 -0
  186. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/utf8.cpp +0 -0
  187. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/value.cpp +0 -0
  188. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/xkcd_colors.cpp +0 -0
  189. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/xkcd_colors.hpp +0 -0
  190. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/src/zip_stored.cpp +0 -0
  191. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/CMakeLists.txt +0 -0
  192. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_bytecode_compiler.cpp +0 -0
  193. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_children_separate.cpp +0 -0
  194. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_cli.cpp +0 -0
  195. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_control_flow.cpp +0 -0
  196. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_coverage.cpp +0 -0
  197. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_csg_tree.cpp +0 -0
  198. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_debug_hooks.cpp +0 -0
  199. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_discretizer.cpp +0 -0
  200. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_dxf_svg_import.cpp +0 -0
  201. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_eval_context.cpp +0 -0
  202. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_export.cpp +0 -0
  203. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_expr_eval.cpp +0 -0
  204. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_extrude_roof.cpp +0 -0
  205. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_feature_detection.cpp +0 -0
  206. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_function_builtins.cpp +0 -0
  207. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_helpers.hpp +0 -0
  208. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_import_export.cpp +0 -0
  209. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_include_cache.cpp +0 -0
  210. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_manifold_cache.cpp +0 -0
  211. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_mesh_check.cpp +0 -0
  212. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_multi_color_merge.cpp +0 -0
  213. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_openscad_parity.cpp +0 -0
  214. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_primitives.cpp +0 -0
  215. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_profile_time.cpp +0 -0
  216. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_profiling.cpp +0 -0
  217. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_python_bindings.py +0 -0
  218. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_render_expr.cpp +0 -0
  219. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_scope_trail.cpp +0 -0
  220. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_scoping.cpp +0 -0
  221. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_surface.cpp +0 -0
  222. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_tail_calls.cpp +0 -0
  223. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_text.cpp +0 -0
  224. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_transforms.cpp +0 -0
  225. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_undef_args.cpp +0 -0
  226. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_unexpected_args.cpp +0 -0
  227. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_use.cpp +0 -0
  228. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_value.cpp +0 -0
  229. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_viewport_params.cpp +0 -0
  230. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tests/test_zip_stored.cpp +0 -0
  231. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tools/cli/CMakeLists.txt +0 -0
  232. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tools/cli/cli_lib.cpp +0 -0
  233. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tools/cli/cli_lib.hpp +0 -0
  234. {openscad_cpp_evaluator-1.46.0 → openscad_cpp_evaluator-1.47.0}/tools/cli/main.cpp +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.46.0
3
+ Version: 1.47.0
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.46.0"
7
+ version = "1.47.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -6,6 +6,8 @@
6
6
 
7
7
  #include <manifold/polygon.h>
8
8
 
9
+ #include <algorithm>
10
+
9
11
  namespace oscadeval {
10
12
 
11
13
  // simplify()'s default tolerance, as a fraction of the body's bounding-box
@@ -663,14 +665,30 @@ std::vector<ColoredBody> generateMeshRepair(Evaluator& ev, const CSGParams& para
663
665
  // The trade is memory: ~67 bytes per cell, so 200^3 is about 550 MB and that
664
666
  // is the practical ceiling. A callback samples lazily and has no ceiling.
665
667
  //
666
- // Accuracy is bounded by the GRID, not by Manifold. Manifold samples on a
667
- // body-centred cubic lattice (two interleaved cubic grids, sdf.cpp:440-446)
668
- // and a plain cubic grid does not line up with those points, so the lambda
669
- // interpolates. Same accuracy as a grid-based marching-cubes implementation
670
- // in script; worse than LevelSet given a true SDF. Do not imply otherwise.
668
+ // Accuracy is bounded by the GRID, not by Manifold. At the grid's own
669
+ // spacing (the default) the grid is marched directly, as BOSL2's
670
+ // isosurface() does -- see marchingCubes(). An edge= asking for a different
671
+ // spacing resamples it through LevelSet instead, which samples a
672
+ // body-centred cubic lattice (sdf.cpp:440-446) that a plain cubic grid does
673
+ // not line up with, so the lambda interpolates trilinearly between samples.
671
674
 
672
675
  namespace {
673
676
 
677
+ // A sample as the field arrays and field functions hand it over. +-INF is a
678
+ // legal sample -- a metaball field is 1/0 wherever a grid point lands on a
679
+ // centre (#744 on BelfrySCAD) -- but it cannot be used as is: the trilinear
680
+ // sampler weights corners, and 0 * INF is NaN. So it becomes a huge FINITE
681
+ // value, which is the limit BOSL2's isosurface() takes (an edge to an INF
682
+ // corner crosses at the other end). A quarter of DBL_MAX so that sums of
683
+ // weighted corners and bandDistance's subtraction cannot overflow. NaN, or
684
+ // anything not a number, has no meaning and is nullopt.
685
+ std::optional<double> fieldSample(const Value& item) {
686
+ const double* d = std::get_if<double>(&item);
687
+ if (!d || std::isnan(*d)) return std::nullopt;
688
+ constexpr double kHuge = std::numeric_limits<double>::max() / 4;
689
+ return std::clamp(*d, -kHuge, kHuge);
690
+ }
691
+
674
692
  struct ScalarField {
675
693
  std::vector<double> v; // flat, x fastest
676
694
  size_t nx = 0, ny = 0, nz = 0;
@@ -698,8 +716,8 @@ std::optional<ScalarField> readField(const Value& val) {
698
716
  return std::nullopt;
699
717
  }
700
718
  for (size_t k = 0; k < f.nz; ++k) {
701
- const double* d = std::get_if<double>(&(*zs)->items[k]);
702
- if (!d || !std::isfinite(*d)) return std::nullopt;
719
+ const std::optional<double> d = fieldSample((*zs)->items[k]);
720
+ if (!d) return std::nullopt;
703
721
  f.v[(k * f.ny + j) * f.nx + i] = *d;
704
722
  }
705
723
  }
@@ -722,8 +740,8 @@ std::optional<std::vector<std::vector<double>>> readPlane(const Value& val) {
722
740
  std::vector<double> row;
723
741
  row.reserve(ny);
724
742
  for (const Value& item : (*ys)->items) {
725
- const double* d = std::get_if<double>(&item);
726
- if (!d || !std::isfinite(*d)) return std::nullopt;
743
+ const std::optional<double> d = fieldSample(item);
744
+ if (!d) return std::nullopt;
727
745
  row.push_back(*d);
728
746
  }
729
747
  out.push_back(std::move(row));
@@ -890,6 +908,162 @@ manifold::Polygons marchingSquares(const Contours2d& g) {
890
908
  }
891
909
 
892
910
 
911
+ // -- 3D: marching cubes on the grid itself -----------------------------------
912
+ //
913
+ // For a grid sampled at the spacing it was given, Manifold's LevelSet is the
914
+ // wrong tool: it samples its own body-centred lattice, which does not line up
915
+ // with the grid, so it meshes a trilinear blend of the data rather than the
916
+ // data. Measured on #745 on BelfrySCAD, that left a metaball field visibly
917
+ // more faceted than BOSL2's isosurface(), which marches the grid itself. So
918
+ // this does too.
919
+ //
920
+ // No 256-case table. The table's hard part is the ambiguous face (two
921
+ // inside corners on a diagonal), and a table that resolves it per CELL can
922
+ // disagree with the neighbouring cell and leave a crack -- fatal here, since
923
+ // Manifold needs a closed mesh. So the triangles are built per FACE instead:
924
+ // each face's crossings are paired into segments by a rule that reads only
925
+ // that face's four values (the face-centre average decides a saddle, as the
926
+ // 2D path does), so the two cells sharing a face always agree. A cell's
927
+ // segments then link into closed loops, one polygon each. Vertices are keyed
928
+ // on GRID EDGE identity, so neighbours share them by construction, exactly as
929
+ // marchingSquares keys on edges rather than coordinates.
930
+ //
931
+ // Orientation: walking a face's corners anticlockwise as seen from outside
932
+ // the cell, a segment runs from an outside->inside crossing to an
933
+ // inside->outside one. A loop of such segments, read in order, is a polygon
934
+ // whose normal points from inside to outside -- Manifold's convention, with
935
+ // inside the positive side.
936
+ //
937
+ // The field arrives as band distance (positive inside) with a ring of
938
+ // "outside" round it, like the 2D path, so every surface closes; the caller
939
+ // clips the overhang back to the bounds.
940
+ manifold::MeshGL64 marchingCubes(const std::vector<double>& d, const size_t n[3],
941
+ const std::array<double, 3>& org, const std::array<double, 3>& h) {
942
+ const auto P = [&](size_t i, size_t j, size_t k) { return (k * n[1] + j) * n[0] + i; };
943
+ // corner c of a cell is offset (c&1, c>>1&1, c>>2&1). Each face's
944
+ // corners, anticlockwise as seen from outside.
945
+ static constexpr int kFaces[6][4] = {
946
+ {0, 2, 3, 1}, {4, 5, 7, 6}, // -z, +z
947
+ {0, 4, 6, 2}, {1, 3, 7, 5}, // -x, +x
948
+ {0, 1, 5, 4}, {2, 6, 7, 3}, // -y, +y
949
+ };
950
+ manifold::MeshGL64 mesh;
951
+ mesh.numProp = 3;
952
+ // ponytail: one int per grid edge, ~12 bytes a sample next to the
953
+ // field's own ~67; key the vertices per slab if this ever binds.
954
+ std::vector<int64_t> edgeVert(3 * n[0] * n[1] * n[2], -1);
955
+ const auto addVert = [&](double x, double y, double z) {
956
+ mesh.vertProperties.insert(mesh.vertProperties.end(), {x, y, z});
957
+ return static_cast<int64_t>(mesh.vertProperties.size() / 3 - 1);
958
+ };
959
+ for (size_t k = 0; k + 1 < n[2]; ++k) {
960
+ for (size_t j = 0; j + 1 < n[1]; ++j) {
961
+ for (size_t i = 0; i + 1 < n[0]; ++i) {
962
+ double v[8];
963
+ size_t p[8];
964
+ int inside = 0;
965
+ for (int c = 0; c < 8; ++c) {
966
+ p[c] = P(i + (c & 1), j + (c >> 1 & 1), k + (c >> 2 & 1));
967
+ v[c] = d[p[c]];
968
+ inside += v[c] > 0.0;
969
+ }
970
+ if (inside == 0 || inside == 8) continue;
971
+ // The vertex on the edge between corners a and b.
972
+ const auto vert = [&](int a, int b) -> int64_t {
973
+ if (a > b) std::swap(a, b); // a is the lower end
974
+ const int axis = (b - a) == 1 ? 0 : (b - a) == 2 ? 1 : 2;
975
+ int64_t& id = edgeVert[3 * p[a] + static_cast<size_t>(axis)];
976
+ if (id < 0) {
977
+ const double t = v[a] / (v[a] - v[b]);
978
+ const double x = org[0] + h[0] * (static_cast<double>(i + (a & 1)) + (axis == 0 ? t : 0.0));
979
+ const double y = org[1] + h[1] * (static_cast<double>(j + (a >> 1 & 1)) + (axis == 1 ? t : 0.0));
980
+ const double z = org[2] + h[2] * (static_cast<double>(k + (a >> 2 & 1)) + (axis == 2 ? t : 0.0));
981
+ id = addVert(x, y, z);
982
+ }
983
+ return id;
984
+ };
985
+ // Segments start->end, at most two per face.
986
+ int64_t segFrom[12], segTo[12];
987
+ int nSeg = 0;
988
+ for (const auto& f : kFaces) {
989
+ int64_t cross[4];
990
+ bool outIn[4];
991
+ int nc = 0;
992
+ for (int m = 0; m < 4; ++m) {
993
+ const int a = f[m], b = f[(m + 1) % 4];
994
+ if ((v[a] > 0.0) != (v[b] > 0.0)) {
995
+ cross[nc] = vert(a, b);
996
+ outIn[nc++] = v[b] > 0.0;
997
+ }
998
+ }
999
+ if (nc == 0) continue;
1000
+ // A saddle joins its inside corners when the face centre
1001
+ // is inside: each out->in crossing then pairs with the
1002
+ // PREVIOUS crossing instead of the next.
1003
+ const bool joined =
1004
+ nc == 4 && v[f[0]] + v[f[1]] + v[f[2]] + v[f[3]] > 0.0;
1005
+ for (int m = 0; m < nc; ++m) {
1006
+ if (!outIn[m]) continue;
1007
+ segFrom[nSeg] = cross[m];
1008
+ segTo[nSeg++] = cross[(m + (joined ? nc - 1 : 1)) % nc];
1009
+ }
1010
+ }
1011
+ // Link into loops; each vertex starts exactly one segment.
1012
+ bool used[12] = {};
1013
+ for (int s0 = 0; s0 < nSeg; ++s0) {
1014
+ if (used[s0]) continue;
1015
+ int64_t loop[12];
1016
+ int len = 0;
1017
+ for (int s = s0; s >= 0 && !used[s];) {
1018
+ used[s] = true;
1019
+ loop[len++] = segFrom[s];
1020
+ const int64_t want = segTo[s];
1021
+ s = -1;
1022
+ for (int q = 0; q < nSeg; ++q)
1023
+ if (!used[q] && segFrom[q] == want) { s = q; break; }
1024
+ }
1025
+ const auto tri = [&](int64_t a, int64_t b, int64_t c) {
1026
+ mesh.triVerts.insert(mesh.triVerts.end(), {static_cast<uint64_t>(a), static_cast<uint64_t>(b),
1027
+ static_cast<uint64_t>(c)});
1028
+ };
1029
+ const auto at = [&](int64_t id, int axis) {
1030
+ return mesh.vertProperties[static_cast<size_t>(id) * 3 + static_cast<size_t>(axis)];
1031
+ };
1032
+ const auto dist2 = [&](int64_t a, int64_t b) {
1033
+ double s2 = 0;
1034
+ for (int ax = 0; ax < 3; ++ax) s2 += (at(a, ax) - at(b, ax)) * (at(a, ax) - at(b, ax));
1035
+ return s2;
1036
+ };
1037
+ if (len == 3) {
1038
+ tri(loop[0], loop[1], loop[2]);
1039
+ } else if (len == 4) {
1040
+ // Split along the shorter diagonal.
1041
+ if (dist2(loop[0], loop[2]) <= dist2(loop[1], loop[3])) {
1042
+ tri(loop[0], loop[1], loop[2]);
1043
+ tri(loop[0], loop[2], loop[3]);
1044
+ } else {
1045
+ tri(loop[1], loop[2], loop[3]);
1046
+ tri(loop[1], loop[3], loop[0]);
1047
+ }
1048
+ } else if (len > 4) {
1049
+ // A fan from one corner of a 5-7 sided, non-planar
1050
+ // loop can fold; a fan from its centroid cannot.
1051
+ double cx = 0, cy = 0, cz = 0;
1052
+ for (int m = 0; m < len; ++m) {
1053
+ cx += at(loop[m], 0);
1054
+ cy += at(loop[m], 1);
1055
+ cz += at(loop[m], 2);
1056
+ }
1057
+ const int64_t c = addVert(cx / len, cy / len, cz / len);
1058
+ for (int m = 0; m < len; ++m) tri(loop[m], loop[(m + 1) % len], c);
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ return mesh;
1065
+ }
1066
+
893
1067
  CSGParams resolveLevelSet(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
894
1068
  auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
895
1069
  CSGParams params;
@@ -1104,8 +1278,7 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1104
1278
  bound.set(fnParams2[1], Value{(*bLo)[1] + static_cast<double>(j) * sy});
1105
1279
  const Value outv =
1106
1280
  ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
1107
- const double* d = std::get_if<double>(&outv);
1108
- const double val = (d && std::isfinite(*d)) ? *d : std::numeric_limits<double>::max();
1281
+ const double val = fieldSample(outv).value_or(std::numeric_limits<double>::max());
1109
1282
  g.v[(j + 1) * g.nx + (i + 1)] = bandDistance(val, isoLo, isoHi, invert);
1110
1283
  }
1111
1284
  }
@@ -1121,6 +1294,7 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1121
1294
  }
1122
1295
 
1123
1296
 
1297
+ bool marchGrid = false;
1124
1298
  if (!fieldFn) {
1125
1299
  field = readField(fieldArg);
1126
1300
  if (!field) {
@@ -1135,6 +1309,15 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1135
1309
  spacing = {((*bHi)[0] - (*bLo)[0]) / static_cast<double>(field->nx - 1),
1136
1310
  ((*bHi)[1] - (*bLo)[1]) / static_cast<double>(field->ny - 1),
1137
1311
  ((*bHi)[2] - (*bLo)[2]) / static_cast<double>(field->nz - 1)};
1312
+ // Marched on the grid itself unless edge= asks for a different
1313
+ // spacing. A 1e-9 relative slack so an edge typed as the spacing,
1314
+ // or computed from the same bounds, still counts as it.
1315
+ const auto same = [](double a, double b) { return std::abs(a - b) <= 1e-9 * std::max(a, b); };
1316
+ marchGrid = edge <= 0.0;
1317
+ if (!marchGrid) {
1318
+ marchGrid = true;
1319
+ for (int a = 0; a < 3; ++a) marchGrid = marchGrid && same(edgeVec ? edgeV[a] : edge, spacing[a]);
1320
+ }
1138
1321
  // Also when edge= was given but unusable (warned above): edge 0
1139
1322
  // would ask Manifold for an infinitely fine lattice.
1140
1323
  if (edge <= 0.0) edge = std::min({spacing[0], spacing[1], spacing[2]});
@@ -1151,10 +1334,9 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1151
1334
  const double xyz[3] = {p.x, p.y, p.z};
1152
1335
  for (int a = 0; a < 3; ++a) bound.set(fnParams2[static_cast<size_t>(a)], Value{xyz[a]});
1153
1336
  const Value out = ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
1154
- const double* d = std::get_if<double>(&out);
1155
1337
  // A field that returns junk at one point must not abort the whole
1156
- // build; treat it as far outside instead.
1157
- const double v = (d && std::isfinite(*d)) ? *d : std::numeric_limits<double>::max();
1338
+ // build; it becomes a huge value instead. +-INF keeps its sign.
1339
+ const double v = fieldSample(out).value_or(std::numeric_limits<double>::max());
1158
1340
  return bandDistance(v, isoLo, isoHi, invert);
1159
1341
  };
1160
1342
 
@@ -1178,18 +1360,13 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1178
1360
  if (i0[a] > n[a] - 2) i0[a] = n[a] - 2;
1179
1361
  t[a] = g[a] - static_cast<double>(i0[a]);
1180
1362
  }
1181
- // Outside the sampled block the answer is "outside", falling away
1182
- // with distance. Clamping instead (which is what the interpolation
1183
- // above does on its own) would smear the boundary values outward and
1184
- // extend the surface rather than close it.
1185
- double outside = 0.0;
1186
- for (int a = 0; a < 3; ++a) {
1187
- const double maxg = static_cast<double>(n[a] - 1);
1188
- const double raw = (pos[a] - origin[a]) / spacing[a];
1189
- if (raw < 0.0) outside = std::max(outside, -raw);
1190
- else if (raw > maxg) outside = std::max(outside, raw - maxg);
1191
- }
1192
- if (outside > 0.0) return invert ? (1.0 + outside) : -(1.0 + outside);
1363
+ // Outside the sampled block the clamping above extends the boundary
1364
+ // values outward, so a surface meeting the box runs straight on into
1365
+ // the padding and the clip to `bounds` below leaves a clean plane.
1366
+ // Answering "outside" there instead (as this once did) made Manifold
1367
+ // close the surface right AT the box face, along its own lattice: a
1368
+ // waffle-textured staircase INSIDE the bounds, which the clip could
1369
+ // not remove (#745 on BelfrySCAD).
1193
1370
 
1194
1371
  double acc = 0.0;
1195
1372
  for (int c = 0; c < 8; ++c) {
@@ -1214,11 +1391,15 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1214
1391
  const manifold::vec3 pad = 2.0 * E;
1215
1392
  manifold::Box bounds(manifold::vec3(origin[0], origin[1], origin[2]) - pad,
1216
1393
  manifold::vec3((*hi)[0], (*hi)[1], (*hi)[2]) + pad);
1217
- // tolerance -1: a positive value makes Manifold do EXTRA evaluations per
1218
- // output vertex to snap nearer the true surface. Against a fixed grid
1219
- // those only re-interpolate data already used -- cost, no information.
1220
- // canParallel true: the lambda is pure C++ and never re-enters the
1221
- // evaluator, which is the whole point of taking a grid.
1394
+ // tolerance: Manifold root-finds along each crossing lattice edge until
1395
+ // the bracket is within it (a distance along the edge, so independent of
1396
+ // the field's units), instead of interpolating linearly between the two
1397
+ // lattice samples. 1% of the edge. Measured on #745 on BelfrySCAD: the
1398
+ // function form's vertices go from up to 0.2 off the true surface to on
1399
+ // it, and the mean face-normal error on a per-axis edge halves to
1400
+ // BOSL2's own; a grid gains too, since the trilinear field between
1401
+ // samples is not linear along a lattice edge. Costs 11-20% more work.
1402
+ //
1222
1403
  // canParallel is the crux: safe for a grid because the lambda is pure
1223
1404
  // C++, and NOT safe for a function, which re-enters the evaluator.
1224
1405
  // Manifold: parallel policies "will crash language runtimes with runtime
@@ -1226,19 +1407,32 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1226
1407
  //
1227
1408
  // LevelSet takes a single edge length, so a per-axis edge meshes a field
1228
1409
  // squashed to unit spacing on every axis and stretches the result back.
1229
- // Exact, not an approximation: with tolerance -1 a vertex is a linear
1230
- // interpolation along a lattice edge, and a linear interpolation commutes
1231
- // with an axis scale.
1410
+ // The tolerance there is 1% of the unit edge, so 1% of each axis's own.
1411
+ const double tol = 0.01 * (edgeVec ? 1.0 : edge);
1232
1412
  manifold::Manifold solid;
1233
- if (!edgeVec) {
1234
- solid = fieldFn ? manifold::Manifold::LevelSet(sampleFn, bounds, edge, 0.0, -1.0, /*canParallel=*/false)
1235
- : manifold::Manifold::LevelSet(sampleGrid, bounds, edge, 0.0, -1.0, /*canParallel=*/true);
1236
- } else {
1413
+ if (marchGrid) {
1414
+ const size_t n[3] = {field->nx + 2, field->ny + 2, field->nz + 2};
1415
+ std::vector<double> d(n[0] * n[1] * n[2], -1.0); // the "outside" ring
1416
+ for (size_t k = 0; k < field->nz; ++k)
1417
+ for (size_t j = 0; j < field->ny; ++j)
1418
+ for (size_t i = 0; i < field->nx; ++i)
1419
+ d[((k + 1) * n[1] + (j + 1)) * n[0] + (i + 1)] =
1420
+ bandDistance(field->at(i, j, k), isoLo, isoHi, invert);
1421
+ const std::array<double, 3> org{origin[0] - spacing[0], origin[1] - spacing[1], origin[2] - spacing[2]};
1422
+ solid = manifold::Manifold(marchingCubes(d, n, org, spacing));
1423
+ // Never expected; but a mesh Manifold refuses must not cost the
1424
+ // user their shape, so fall back to resampling it.
1425
+ if (solid.Status() != manifold::Manifold::Error::NoError) marchGrid = false;
1426
+ }
1427
+ if (!marchGrid && !edgeVec) {
1428
+ solid = fieldFn ? manifold::Manifold::LevelSet(sampleFn, bounds, edge, 0.0, tol, /*canParallel=*/false)
1429
+ : manifold::Manifold::LevelSet(sampleGrid, bounds, edge, 0.0, tol, /*canParallel=*/true);
1430
+ } else if (!marchGrid) {
1237
1431
  const manifold::Box unit(bounds.min / E, bounds.max / E);
1238
1432
  solid = fieldFn ? manifold::Manifold::LevelSet([&](manifold::vec3 q) { return sampleFn(q * E); },
1239
- unit, 1.0, 0.0, -1.0, /*canParallel=*/false)
1433
+ unit, 1.0, 0.0, tol, /*canParallel=*/false)
1240
1434
  : manifold::Manifold::LevelSet([&](manifold::vec3 q) { return sampleGrid(q * E); },
1241
- unit, 1.0, 0.0, -1.0, /*canParallel=*/true);
1435
+ unit, 1.0, 0.0, tol, /*canParallel=*/true);
1242
1436
  solid = solid.Scale(E);
1243
1437
  }
1244
1438
 
@@ -1192,6 +1192,60 @@ TEST(LevelSet, MalformedFieldsWarn) {
1192
1192
  }
1193
1193
  }
1194
1194
 
1195
+ TEST(LevelSet, InfinityIsASampleNotAMalformedField) {
1196
+ // INF is BOSL2's name for 1/0, not a builtin, so each script defines it.
1197
+ // #744 on BelfrySCAD: a metaball field is 1/0 where a grid point lands on
1198
+ // a centre. The field is rectangular; INF made it read as malformed and
1199
+ // nothing was drawn. A metaball r/d around the origin, centre sample INF:
1200
+ // [1, INF] is the ball of radius r.
1201
+ const std::string field =
1202
+ "INF = 1/0; N = 21; H = 10;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
1203
+ "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
1204
+ " let(d = norm([co(i), co(j), co(k)])) d == 0 ? INF : 6 / d ]]];\n";
1205
+ const std::vector<std::string> w =
1206
+ levelsetWarnings(field + "levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=[1, INF]);");
1207
+ EXPECT_TRUE(w.empty()) << w.front();
1208
+ Evaluated e = evalSrc(field + "levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=[1, INF]);");
1209
+ ASSERT_EQ(e.bodies.size(), 1u);
1210
+ const double analytic = 4.0 / 3.0 * 3.14159265358979 * 6 * 6 * 6;
1211
+ EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.05 * analytic);
1212
+ EXPECT_EQ(soleBody(e).Genus(), 0);
1213
+
1214
+ // -INF keeps its sign: the same ball, as a field that is -INF at the centre.
1215
+ Evaluated neg = evalSrc(
1216
+ "INF = 1/0; N = 21; H = 10;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
1217
+ "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
1218
+ " let(d = norm([co(i), co(j), co(k)])) d == 0 ? -INF : -6 / d ]]];\n"
1219
+ "levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=-1);");
1220
+ ASSERT_EQ(neg.bodies.size(), 1u);
1221
+ EXPECT_NEAR(soleBody(neg).Volume(), analytic, 0.05 * analytic);
1222
+
1223
+ // And the 2D reader the same way.
1224
+ EXPECT_TRUE(levelsetWarnings(
1225
+ "INF = 1/0; f = [for (i=[-5:5]) [for (j=[-5:5]) let(d = norm([i,j])) d == 0 ? INF : 3 / d]];\n"
1226
+ "levelset(f, bounds=[[-5,-5],[5,5]], isovalue=[1, INF]);")
1227
+ .empty());
1228
+ }
1229
+
1230
+ TEST(LevelSet, NanIsStillAMalformedField) {
1231
+ EXPECT_FALSE(levelsetWarnings(
1232
+ "levelset([[[1,2],[3,0/0]],[[5,6],[7,8]]], bounds=[[0,0,0],[1,1,1]]);").empty());
1233
+ }
1234
+
1235
+ TEST(LevelSet, AFunctionReturningMinusInfinityIsFarBelowNotFarAbove) {
1236
+ // Every non-finite value used to become +DBL_MAX, so -INF counted as
1237
+ // ABOVE every isovalue -- outside a scalar one. -INF over a ball of
1238
+ // radius 6 and 0 elsewhere: at or below -1 is that ball, not nothing.
1239
+ const std::string src =
1240
+ "INF = 1/0; levelset(function(x,y,z) norm([x,y,z]) < 6 ? -INF : 0,\n"
1241
+ " bounds=[[-10,-10,-10],[10,10,10]], isovalue=-1, edge=0.5);";
1242
+ EXPECT_TRUE(levelsetWarnings(src).empty());
1243
+ Evaluated e = evalSrc(src);
1244
+ ASSERT_EQ(e.bodies.size(), 1u);
1245
+ const double analytic = 4.0 / 3.0 * 3.14159265358979 * 6 * 6 * 6;
1246
+ EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.1 * analytic);
1247
+ }
1248
+
1195
1249
  // -- levelset, function form ----------------------------------------------
1196
1250
  //
1197
1251
  // Same builtin, field given as function(x,y,z) instead of a grid. Slower --
@@ -1283,8 +1337,10 @@ TEST(LevelSetFn, AVectorEdgeIsExactlyAStretchedLattice) {
1283
1337
  }
1284
1338
 
1285
1339
  TEST(LevelSetFn, AVectorEdgeAlsoAppliesToAGrid) {
1340
+ // Compared with an isotropic resample, not the default: the default
1341
+ // marches the grid itself, which is a different mesher.
1286
1342
  Evaluated fine = evalSrc(sphereFieldSrc(50, 30) +
1287
- "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
1343
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20, edge=1.2);");
1288
1344
  Evaluated coarseZ = evalSrc(sphereFieldSrc(50, 30) +
1289
1345
  "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20, edge=[1.2,1.2,4]);");
1290
1346
  ASSERT_EQ(coarseZ.bodies.size(), 1u);
@@ -1511,15 +1567,159 @@ TEST(LevelSet, PaddingDoesNotDisturbAShapeInsideTheBox) {
1511
1567
  }
1512
1568
 
1513
1569
  TEST(LevelSet, AGridFieldIsAlsoCutCleanlyAtTheBox) {
1514
- // The grid intake cannot sample beyond its data, so the sampler reads
1515
- // "outside" past the block rather than clamping and smearing the edge
1516
- // values outward. Same clean cut as the function form.
1570
+ // The grid intake cannot sample beyond its data, so past the block the
1571
+ // sampler clamps -- the edge values run on into the padding -- and the
1572
+ // clip to the bounds makes the cut. It used to read "outside" there
1573
+ // instead, which closed the surface AT the box face along Manifold's
1574
+ // lattice: a waffle-textured staircase inside the bounds that the clip
1575
+ // could not reach (#745 on BelfrySCAD). This test then needed one
1576
+ // sample layer of slack; a clean cut needs none.
1577
+ for (const char* edge : {"", ", edge=2", ", edge=[1,2,0.5]"}) {
1578
+ Evaluated e = evalSrc(
1579
+ std::string("N = 41;\nfunction co(t) = -20 + 40*t/(N-1);\n"
1580
+ "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1]) co(i) ]]];\n"
1581
+ "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=[-1e18,0]") + edge + ");");
1582
+ ASSERT_EQ(e.bodies.size(), 1u) << edge;
1583
+ EXPECT_NEAR(soleBody(e).Volume(), 32000.0, 1e-6) << edge;
1584
+ }
1585
+ // And a curved surface the box cuts through: a sphere of radius 25 in a
1586
+ // box of half-width 20. The faces lying flat on x=20 together make the
1587
+ // full disc the plane cuts, not a staircase short of it.
1588
+ Evaluated s = evalSrc(sphereFieldSrc(41, 20) +
1589
+ "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=25);");
1590
+ ASSERT_EQ(s.bodies.size(), 1u);
1591
+ const manifold::MeshGL m = soleBody(s).GetMeshGL();
1592
+ double capArea = 0;
1593
+ for (size_t t = 0; t + 2 < m.triVerts.size(); t += 3) {
1594
+ manifold::vec3 p[3];
1595
+ for (int k = 0; k < 3; ++k) {
1596
+ const size_t v = static_cast<size_t>(m.triVerts[t + static_cast<size_t>(k)]) * m.numProp;
1597
+ p[k] = manifold::vec3(m.vertProperties[v], m.vertProperties[v + 1], m.vertProperties[v + 2]);
1598
+ }
1599
+ if (std::abs(p[0].x - 20) < 1e-6 && std::abs(p[1].x - 20) < 1e-6 && std::abs(p[2].x - 20) < 1e-6)
1600
+ capArea += 0.5 * manifold::la::length(manifold::la::cross(p[1] - p[0], p[2] - p[0]));
1601
+ }
1602
+ // The plane x=20 cuts the sphere in a disc of radius sqrt(25^2 - 20^2)
1603
+ // = 15, which fits inside the 40x40 face.
1604
+ const double r = 15.0;
1605
+ EXPECT_NEAR(capArea, 3.14159265358979 * r * r, 0.03 * 3.14159265358979 * r * r);
1606
+ }
1607
+
1608
+ TEST(LevelSetFn, VerticesLieOnTheTrueSurface) {
1609
+ // A positive tolerance makes Manifold root-find along each lattice edge
1610
+ // instead of interpolating linearly between its two samples, so a
1611
+ // function field's vertices land on the surface itself. Measured on
1612
+ // #745 on BelfrySCAD: 0.2 off at edge=2 before, on the surface after;
1613
+ // the coarse, curved 1/r field there is where linear interpolation is
1614
+ // worst.
1615
+ for (const char* edge : {"2", "[1,2,0.4]"}) {
1616
+ Evaluated e = evalSrc(std::string("levelset(function(x,y,z) 5 / norm([x,y,z]), "
1617
+ "bounds=[[-8,-8,-8],[8,8,8]], isovalue=[1, 1/0], edge=") +
1618
+ edge + ");");
1619
+ ASSERT_EQ(e.bodies.size(), 1u) << edge;
1620
+ const manifold::MeshGL m = soleBody(e).GetMeshGL();
1621
+ double worst = 0;
1622
+ for (size_t v = 0; v + 2 < m.vertProperties.size(); v += m.numProp)
1623
+ worst = std::max(worst, std::abs(manifold::la::length(manifold::vec3(m.vertProperties[v], m.vertProperties[v + 1],
1624
+ m.vertProperties[v + 2])) - 5.0));
1625
+ EXPECT_LT(worst, 0.03) << edge;
1626
+ }
1627
+ }
1628
+
1629
+ // -- levelset, grid marched directly ---------------------------------------
1630
+ //
1631
+ // At its own spacing a grid is marched as it stands (marchingCubes), not
1632
+ // resampled through Manifold's body-centred lattice -- which meshed a
1633
+ // trilinear blend of the data and left #745 on BelfrySCAD's metaballs more
1634
+ // faceted than BOSL2's isosurface(). The signature of marching the grid: a
1635
+ // vertex sits on a grid EDGE, so at least two of its coordinates are grid
1636
+ // lines. Manifold's lattice puts most of them on diagonals. The mesher also
1637
+ // falls back to resampling if Manifold ever refused its mesh, and this is
1638
+ // what would catch that happening silently.
1639
+
1640
+ namespace {
1641
+
1642
+ // Fraction of the body's vertices with two or more coordinates on the grid
1643
+ // lines lo + i*h. Polygons of 5+ sides get a centroid vertex, which does not
1644
+ // qualify, so marching the grid scores 0.66-0.81 on the fields below.
1645
+ // Resampling at an edge 1e-6 away from the grid's scores 0.001-0.024.
1646
+ double onGridLines(const manifold::Manifold& m, manifold::vec3 lo, manifold::vec3 h) {
1647
+ const manifold::MeshGL g = m.GetMeshGL();
1648
+ size_t on = 0, all = 0;
1649
+ for (size_t v = 0; v + 2 < g.vertProperties.size(); v += g.numProp, ++all) {
1650
+ int lines = 0;
1651
+ for (int a = 0; a < 3; ++a) {
1652
+ const double t = (g.vertProperties[v + static_cast<size_t>(a)] - lo[a]) / h[a];
1653
+ lines += std::abs(t - std::round(t)) < 1e-4;
1654
+ }
1655
+ on += lines >= 2;
1656
+ }
1657
+ return all ? static_cast<double>(on) / static_cast<double>(all) : 0.0;
1658
+ }
1659
+
1660
+ } // namespace
1661
+
1662
+ TEST(LevelSetGrid, TheDefaultMarchesTheGridItself) {
1663
+ Evaluated e = evalSrc(sphereFieldSrc(41, 20) +
1664
+ "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=15);");
1665
+ ASSERT_EQ(e.bodies.size(), 1u);
1666
+ EXPECT_GT(onGridLines(soleBody(e), manifold::vec3(-20.0), manifold::vec3(1.0)), 0.5);
1667
+ EXPECT_EQ(soleBody(e).Genus(), 0);
1668
+ const double analytic = 4.0 / 3.0 * 3.14159265358979 * 15 * 15 * 15;
1669
+ EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.01 * analytic);
1670
+ // And edge= naming the grid's own spacing is the same request.
1671
+ Evaluated same = evalSrc(sphereFieldSrc(41, 20) +
1672
+ "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=15, edge=[1,1,1]);");
1673
+ ASSERT_EQ(same.bodies.size(), 1u);
1674
+ EXPECT_EQ(soleBody(same).NumTri(), soleBody(e).NumTri());
1675
+ }
1676
+
1677
+ TEST(LevelSetGrid, ADifferentEdgeStillResamples) {
1678
+ Evaluated e = evalSrc(sphereFieldSrc(41, 20) +
1679
+ "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=15, edge=0.7);");
1680
+ ASSERT_EQ(e.bodies.size(), 1u);
1681
+ EXPECT_LT(onGridLines(soleBody(e), manifold::vec3(-20.0), manifold::vec3(1.0)), 0.1);
1682
+ }
1683
+
1684
+ TEST(LevelSetGrid, SaddlesEverywhereStillCloseTheMesh) {
1685
+ // Random samples: ambiguous faces in every direction, which is where a
1686
+ // per-cell table cracks. A crack would fail Manifold's check and fall
1687
+ // back to resampling, so the vertices would leave the grid lines.
1688
+ for (const char* iso : {"0", "[-0.3, 0.4], invert=true"}) {
1689
+ Evaluated e = evalSrc(std::string("r = rands(-1, 1, 20*20*20, 42);\n"
1690
+ "f = [for (i=[0:19]) [for (j=[0:19]) [for (k=[0:19]) r[(i*20+j)*20+k] ]]];\n"
1691
+ "levelset(f, bounds=[[0,0,0],[19,19,19]], isovalue=") + iso + ");");
1692
+ ASSERT_EQ(e.bodies.size(), 1u) << iso;
1693
+ EXPECT_GT(onGridLines(soleBody(e), manifold::vec3(0.0), manifold::vec3(1.0)), 0.5) << iso;
1694
+ EXPECT_GT(soleBody(e).Volume(), 0.0) << iso;
1695
+ }
1696
+ }
1697
+
1698
+ TEST(LevelSetGrid, SamplesExactlyOnTheIsovalueStayManifold) {
1699
+ // Integer data at an integer isovalue: crossings land ON grid corners,
1700
+ // so several edges share a vertex position. The old resample of this
1701
+ // exported 4 non-manifold edges and 2 duplicate faces.
1517
1702
  Evaluated e = evalSrc(
1518
- "N = 41;\nfunction co(t) = -20 + 40*t/(N-1);\n"
1519
- "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1]) co(i) ]]];\n"
1520
- "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=[-1e18,0]);");
1703
+ "f = [for (i=[0:9]) [for (j=[0:9]) [for (k=[0:9]) (i+j+k) % 3 ]]];\n"
1704
+ "levelset(f, bounds=[[0,0,0],[9,9,9]], isovalue=1);");
1521
1705
  ASSERT_EQ(e.bodies.size(), 1u);
1522
- EXPECT_NEAR(soleBody(e).Volume(), 32000.0, 40.0); // one sample layer of slack
1706
+ EXPECT_EQ(soleBody(e).Status(), manifold::Manifold::Error::NoError);
1707
+ EXPECT_GT(onGridLines(soleBody(e), manifold::vec3(0.0), manifold::vec3(1.0)), 0.5);
1708
+ }
1709
+
1710
+ TEST(LevelSetGrid, UnequalSpacingPerAxisIsMarchedAsIs) {
1711
+ // 31 samples over 20 x 40 x 10: three different spacings, no common edge
1712
+ // to resample at. A unit-ball field becomes an ellipsoid of semi-axes
1713
+ // 8, 16 and 4 at isovalue 0.8.
1714
+ Evaluated e = evalSrc(
1715
+ "N = 31;\nf = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
1716
+ " norm([(i-15)/15, (j-15)/15, (k-15)/15]) ]]];\n"
1717
+ "levelset(f, bounds=[[-10,-20,-5],[10,20,5]], isovalue=0.8);");
1718
+ ASSERT_EQ(e.bodies.size(), 1u);
1719
+ EXPECT_GT(onGridLines(soleBody(e), manifold::vec3(-10, -20, -5), manifold::vec3(20.0 / 30, 40.0 / 30, 10.0 / 30)),
1720
+ 0.5);
1721
+ const double analytic = 4.0 / 3.0 * 3.14159265358979 * 8 * 16 * 4;
1722
+ EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.02 * analytic);
1523
1723
  }
1524
1724
 
1525
1725
  // -- 2D union keeps every colour (bug report: "multiple colors inside a
@@ -55,7 +55,7 @@ wheels = [
55
55
 
56
56
  [[package]]
57
57
  name = "openscad-cpp-evaluator"
58
- version = "1.46.0"
58
+ version = "1.47.0"
59
59
  source = { editable = "." }
60
60
  dependencies = [
61
61
  { name = "numpy" },