openscad-cpp-evaluator 1.47.0__tar.gz → 1.49.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.47.0 → openscad_cpp_evaluator-1.49.0}/CLAUDE.md +6 -0
  2. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/function_builtins.cpp +1 -1
  5. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/topology.cpp +171 -131
  6. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_booleans.cpp +101 -10
  7. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/uv.lock +1 -1
  8. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/.github/workflows/ci.yml +0 -0
  9. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/.github/workflows/wheels.yml +0 -0
  10. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/.gitignore +0 -0
  11. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/.gitmodules +0 -0
  12. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/CMakeLists.txt +0 -0
  13. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/LICENSE +0 -0
  14. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/README.md +0 -0
  15. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/bindings/CMakeLists.txt +0 -0
  16. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/bindings/ast_to_py.cpp +0 -0
  17. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/bindings/module.cpp +0 -0
  18. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/cmake/embed_font.cmake +0 -0
  19. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/cmake/patch_manifold.cmake +0 -0
  20. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/examples/CMakeLists.txt +0 -0
  21. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/examples/eval_perf_benchmark.cpp +0 -0
  22. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/examples/manifold_cache_reuse.cpp +0 -0
  23. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/examples/minimal_debugger.cpp +0 -0
  24. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  25. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/.gitignore +0 -0
  26. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  27. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  28. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/LICENSE +0 -0
  29. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/README.md +0 -0
  30. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  31. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  32. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  33. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  34. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  35. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  36. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  37. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  38. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  39. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  40. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  41. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  42. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  43. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  44. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  45. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  46. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/.DS_Store +0 -0
  47. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  48. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  49. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  50. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  51. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  52. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  53. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  54. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  55. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  56. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  57. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  58. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  59. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  60. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  61. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  62. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  63. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  64. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  65. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  66. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  67. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  68. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  69. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  70. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  71. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  72. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  73. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  74. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  75. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_concurrent_parse.cpp +0 -0
  76. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  77. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  78. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  79. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  80. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  81. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  82. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  83. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  84. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  85. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  86. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_profile_time.cpp +0 -0
  87. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  88. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  89. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  90. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  91. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  92. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  93. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  94. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  95. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  96. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  97. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  98. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  99. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  100. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  101. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  102. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  103. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  104. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  105. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  106. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  107. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  108. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  109. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  110. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  111. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  112. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  113. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  114. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  115. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  116. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  117. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  118. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  119. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  120. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  121. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  122. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  123. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  124. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  125. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  126. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  127. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  128. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  129. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  130. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  131. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  132. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  133. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  134. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/resources/fonts/LICENSE +0 -0
  135. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  136. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  137. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  138. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  139. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/CMakeLists.txt +0 -0
  140. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/booleans.cpp +0 -0
  141. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/builtins.hpp +0 -0
  142. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/call_args.cpp +0 -0
  143. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/color.cpp +0 -0
  144. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/control.cpp +0 -0
  145. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/dxf_dim.cpp +0 -0
  146. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/extrude.cpp +0 -0
  147. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/import.cpp +0 -0
  148. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/modifiers.cpp +0 -0
  149. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/primitives_2d.cpp +0 -0
  150. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/primitives_3d.cpp +0 -0
  151. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/profile_time.cpp +0 -0
  152. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/registry.cpp +0 -0
  153. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/roof.cpp +0 -0
  154. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/segments.cpp +0 -0
  155. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/surface.cpp +0 -0
  156. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/text.cpp +0 -0
  157. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/builtins/transforms.cpp +0 -0
  158. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/bytecode_compiler.cpp +0 -0
  159. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/bytecode_vm.cpp +0 -0
  160. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/colored_body.cpp +0 -0
  161. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/coverage.cpp +0 -0
  162. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/csg_generate.cpp +0 -0
  163. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/csg_resolve.cpp +0 -0
  164. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/css_colors.cpp +0 -0
  165. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/debug/debug_repl.cpp +0 -0
  166. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/debug_profile.cpp +0 -0
  167. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/eval_context.cpp +0 -0
  168. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/eval_error.cpp +0 -0
  169. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/eval_use.cpp +0 -0
  170. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/export.cpp +0 -0
  171. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/expr_eval.cpp +0 -0
  172. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/format_closure.cpp +0 -0
  173. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/import/dxf_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/import/mesh_import.cpp +0 -0
  175. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/import/scene_import.cpp +0 -0
  176. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/import/surface_load.cpp +0 -0
  177. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/import/svg_import.cpp +0 -0
  178. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/manifold_cache.cpp +0 -0
  179. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/measure_geometry.cpp +0 -0
  180. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/mesh_check.cpp +0 -0
  181. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/stmt_eval.cpp +0 -0
  182. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/text/font_match.cpp +0 -0
  183. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/text/freetype_font_provider.cpp +0 -0
  184. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/text/text_metrics.cpp +0 -0
  185. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/user_calls.cpp +0 -0
  186. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/utf8.cpp +0 -0
  187. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/value.cpp +0 -0
  188. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/xkcd_colors.cpp +0 -0
  189. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/xkcd_colors.hpp +0 -0
  190. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/src/zip_stored.cpp +0 -0
  191. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/CMakeLists.txt +0 -0
  192. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_bytecode_compiler.cpp +0 -0
  193. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_children_separate.cpp +0 -0
  194. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_cli.cpp +0 -0
  195. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_control_flow.cpp +0 -0
  196. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_coverage.cpp +0 -0
  197. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_csg_tree.cpp +0 -0
  198. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_debug_hooks.cpp +0 -0
  199. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_discretizer.cpp +0 -0
  200. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_dxf_svg_import.cpp +0 -0
  201. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_eval_context.cpp +0 -0
  202. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_export.cpp +0 -0
  203. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_expr_eval.cpp +0 -0
  204. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_extrude_roof.cpp +0 -0
  205. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_feature_detection.cpp +0 -0
  206. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_function_builtins.cpp +0 -0
  207. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_helpers.hpp +0 -0
  208. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_import_export.cpp +0 -0
  209. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_include_cache.cpp +0 -0
  210. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_manifold_cache.cpp +0 -0
  211. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_mesh_check.cpp +0 -0
  212. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_multi_color_merge.cpp +0 -0
  213. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_openscad_parity.cpp +0 -0
  214. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_primitives.cpp +0 -0
  215. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_profile_time.cpp +0 -0
  216. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_profiling.cpp +0 -0
  217. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_python_bindings.py +0 -0
  218. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_render_expr.cpp +0 -0
  219. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_scope_trail.cpp +0 -0
  220. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_scoping.cpp +0 -0
  221. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_surface.cpp +0 -0
  222. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_tail_calls.cpp +0 -0
  223. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_text.cpp +0 -0
  224. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_transforms.cpp +0 -0
  225. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_undef_args.cpp +0 -0
  226. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_unexpected_args.cpp +0 -0
  227. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_use.cpp +0 -0
  228. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_value.cpp +0 -0
  229. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_viewport_params.cpp +0 -0
  230. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tests/test_zip_stored.cpp +0 -0
  231. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tools/cli/CMakeLists.txt +0 -0
  232. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tools/cli/cli_lib.cpp +0 -0
  233. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tools/cli/cli_lib.hpp +0 -0
  234. {openscad_cpp_evaluator-1.47.0 → openscad_cpp_evaluator-1.49.0}/tools/cli/main.cpp +0 -0
@@ -606,6 +606,12 @@ grep for `ponytail:`.
606
606
  field squashed to unit spacing and `Scale`s the result back -- exact, since with tolerance -1 a
607
607
  vertex is a linear interpolation along a lattice edge, which commutes with an axis scale. A plain
608
608
  number takes the old path unchanged. `supported_feature("levelset")` is **2** from this change.
609
+ **A field array needs `bounds` OR `edge`**: given only `edge` (number or vector), the samples sit
610
+ `edge` apart **centred on the origin**, as BOSL2's `isosurface()` places an array given
611
+ `voxel_size`, and the array's depth (`field[i][j]` vs `field[i][j][k]`) decides 2D or 3D
612
+ (`boundsFromEdge`, #749 on BelfrySCAD). Both given: `edge` re-samples the field inside `bounds`
613
+ (trilinear in 3D, bilinear in 2D -- 2D used to ignore `edge` here) unless it equals the grid's own spacing. A function field still needs `bounds`.
614
+ `supported_feature("levelset")` is **3** from this change.
609
615
  **The field is a grid OR a `function(x,y,z)`, and the two trade off against each other** —
610
616
  neither is simply better:
611
617
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.47.0
3
+ Version: 1.49.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.47.0"
7
+ version = "1.49.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -987,7 +987,7 @@ const std::unordered_map<std::string, double>& featureLevels() {
987
987
  static const std::unordered_map<std::string, double> levels = {
988
988
  {"render-expr", 1.0}, // render() in expression position
989
989
  {"linear-solve", 1.0}, // linear_solve(A, b) -> {x, det, singular}
990
- {"levelset", 2.0}, // levelset(field, bounds, isovalue); 2: edge may be a vector
990
+ {"levelset", 3.0}, // levelset(field, bounds, isovalue); 2: edge may be a vector; 3: edge alone places a field array
991
991
  {"polyhedron-vnf", 1.0}, // polyhedron(vnf) / polyhedron(object)
992
992
  {"separate-children", 1.0}, // children(..., separate=true)
993
993
  {"minkowski-diff", 1.0}, // minkowski_difference()
@@ -7,6 +7,7 @@
7
7
  #include <manifold/polygon.h>
8
8
 
9
9
  #include <algorithm>
10
+ #include <utility>
10
11
 
11
12
  namespace oscadeval {
12
13
 
@@ -650,27 +651,22 @@ std::vector<ColoredBody> generateMeshRepair(Evaluator& ev, const CSGParams& para
650
651
 
651
652
  // -- levelset(field, bounds, isovalue) -------------------------------------
652
653
  //
653
- // A solid from a grid of sampled values. Wraps Manifold::LevelSet with a C++
654
- // trilinear-sampling lambda over the array the script handed us.
654
+ // A solid from a grid of sampled values: the field array the script handed
655
+ // us, or a function sampled onto a grid at edge= spacing. The grid is then
656
+ // marched directly, as BOSL2's isosurface() does -- see marchingCubes() and
657
+ // the end of generateLevelSet for why Manifold::LevelSet is no longer used.
655
658
  //
656
- // Why a grid rather than Manifold's own SDF-callback shape, which would be
657
- // the obvious mapping: measured, building the field in OpenSCAD costs
658
- // 0.41 us/sample against 0.96 us for a closure call per sample, because the
659
- // arithmetic inlines into the list comprehension. More importantly a grid
660
- // means the C++ side never re-enters the evaluator, so canParallel can be
661
- // TRUE -- Manifold's docs warn that parallel policies "will crash language
662
- // runtimes with runtime locks that expect to not be called back by
663
- // unregistered threads", which is exactly what a callback design forfeits.
659
+ // Why offer an array at all, when a function is the obvious mapping:
660
+ // measured, building the field in OpenSCAD costs 0.41 us/sample against
661
+ // 0.96 us for a closure call per sample, because the arithmetic inlines into
662
+ // the list comprehension.
664
663
  //
665
- // The trade is memory: ~67 bytes per cell, so 200^3 is about 550 MB and that
666
- // is the practical ceiling. A callback samples lazily and has no ceiling.
664
+ // The trade is memory: ~67 bytes per cell for the array, so 200^3 is about
665
+ // 550 MB and that is the practical ceiling.
667
666
  //
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.
667
+ // Accuracy is bounded by the GRID. An array at its own spacing (the default)
668
+ // is marched as is; an edge= asking for a different spacing re-samples it
669
+ // trilinearly onto a grid at that spacing first.
674
670
 
675
671
  namespace {
676
672
 
@@ -689,6 +685,44 @@ std::optional<double> fieldSample(const Value& item) {
689
685
  return std::clamp(*d, -kHuge, kHuge);
690
686
  }
691
687
 
688
+ // bounds for a field ARRAY given edge= and no bounds (#749 on BelfrySCAD):
689
+ // the samples edge apart, centred on the origin, as BOSL2's isosurface()
690
+ // places an array given voxel_size. The array's depth (field[i][j] or
691
+ // field[i][j][k]) says 2D or 3D; the sizes come from its first row, and
692
+ // readField/readPlane check the rest later. nullopt when edge is missing
693
+ // or does not fit, leaving the caller's "bounds must be ..." warning.
694
+ std::optional<std::pair<std::vector<double>, std::vector<double>>>
695
+ boundsFromEdge(const Value& field, const Value& edge) {
696
+ std::vector<size_t> dims;
697
+ for (const Value* cur = &field; dims.size() < 3;) {
698
+ const ListPtr* l = std::get_if<ListPtr>(cur);
699
+ if (!l || !*l || (*l)->items.empty()) break;
700
+ dims.push_back((*l)->items.size());
701
+ cur = &(*l)->items[0];
702
+ }
703
+ if (dims.size() < 2) return std::nullopt;
704
+ std::vector<double> step;
705
+ if (const double* e = std::get_if<double>(&edge)) {
706
+ step.assign(dims.size(), *e);
707
+ } else if (const ListPtr* l = std::get_if<ListPtr>(&edge); l && *l && (*l)->items.size() == dims.size()) {
708
+ for (const Value& item : (*l)->items) {
709
+ const double* d = std::get_if<double>(&item);
710
+ if (!d) return std::nullopt;
711
+ step.push_back(*d);
712
+ }
713
+ } else {
714
+ return std::nullopt;
715
+ }
716
+ std::vector<double> lo, hi;
717
+ for (size_t a = 0; a < dims.size(); ++a) {
718
+ if (!std::isfinite(step[a]) || step[a] <= 0.0 || dims[a] < 2) return std::nullopt;
719
+ const double half = 0.5 * step[a] * static_cast<double>(dims[a] - 1);
720
+ lo.push_back(-half);
721
+ hi.push_back(half);
722
+ }
723
+ return std::make_pair(lo, hi);
724
+ }
725
+
692
726
  struct ScalarField {
693
727
  std::vector<double> v; // flat, x fastest
694
728
  size_t nx = 0, ny = 0, nz = 0;
@@ -974,6 +1008,13 @@ manifold::MeshGL64 marchingCubes(const std::vector<double>& d, const size_t n[3]
974
1008
  const int axis = (b - a) == 1 ? 0 : (b - a) == 2 ? 1 : 2;
975
1009
  int64_t& id = edgeVert[3 * p[a] + static_cast<size_t>(axis)];
976
1010
  if (id < 0) {
1011
+ // Linear, as BOSL2 interpolates. Root-finding each
1012
+ // vertex onto the true surface of a function field was
1013
+ // tried and measured (#745 on BelfrySCAD): 16% less mean
1014
+ // normal error on a sphere, but where the grid is too
1015
+ // coarse for the field it put real ripples on vertices
1016
+ // the grid cannot connect -- jagged, sliver-strewn --
1017
+ // where interpolating smooths them away, as BOSL2 does.
977
1018
  const double t = v[a] / (v[a] - v[b]);
978
1019
  const double x = org[0] + h[0] * (static_cast<double>(i + (a & 1)) + (axis == 0 ? t : 0.0));
979
1020
  const double y = org[1] + h[1] * (static_cast<double>(j + (a >> 1 & 1)) + (axis == 1 ? t : 0.0));
@@ -1083,14 +1124,10 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1083
1124
  const oscad::ASTNode& node) {
1084
1125
  // Two shapes of field, and the choice matters beyond taste:
1085
1126
  //
1086
- // a GRID -- faster to sample (0.41 vs 0.96 us) and meshes in
1087
- // PARALLEL, because nothing re-enters the evaluator.
1088
- // Accuracy is capped by the grid; memory is the ceiling.
1089
- // a FUNCTION -- Manifold picks its own sample points and can snap to
1090
- // the true surface, so it is MORE accurate, with no
1091
- // memory ceiling. But every sample is a closure call, and
1092
- // canParallel must be false or the evaluator gets called
1093
- // back from threads it knows nothing about.
1127
+ // a GRID -- faster to sample (0.41 vs 0.96 us). Accuracy is capped
1128
+ // by the grid; memory is the ceiling.
1129
+ // a FUNCTION -- sampled onto a grid at edge= spacing, one closure call
1130
+ // per sample. Needs edge=, since nothing else says how fine.
1094
1131
  const Value& fieldArg = params.at("field");
1095
1132
  const ClosurePtr* fieldFn = std::get_if<ClosurePtr>(&fieldArg);
1096
1133
  // The array is NOT parsed yet: whether it should be field[i][j] or
@@ -1108,12 +1145,20 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1108
1145
  // matches how the caller already has to think about the box.
1109
1146
  const ListPtr* bb = std::get_if<ListPtr>(&params.at("bounds"));
1110
1147
  std::optional<std::vector<double>> bLo, bHi;
1148
+ const bool noBounds = std::holds_alternative<std::monostate>(params.at("bounds"));
1111
1149
  if (bb && *bb && (*bb)->items.size() == 2) {
1112
1150
  bLo = numbersOrInf((*bb)->items[0]);
1113
1151
  bHi = numbersOrInf((*bb)->items[1]);
1152
+ } else if (noBounds && !fieldFn) {
1153
+ if (const auto inferred = boundsFromEdge(fieldArg, params.at("edge"))) {
1154
+ bLo = inferred->first;
1155
+ bHi = inferred->second;
1156
+ }
1114
1157
  }
1115
1158
  if (!bLo || !bHi || bLo->size() != bHi->size() || (bLo->size() != 2 && bLo->size() != 3)) {
1116
- ev.warn("levelset(): bounds must be [[x0,y0],[x1,y1]] or [[x0,y0,z0],[x1,y1,z1]]",
1159
+ ev.warn(noBounds && !fieldFn
1160
+ ? "levelset(): a field array needs bounds=, or edge= (its sample spacing) to place it"
1161
+ : "levelset(): bounds must be [[x0,y0],[x1,y1]] or [[x0,y0,z0],[x1,y1,z1]]",
1117
1162
  &node.position());
1118
1163
  return {};
1119
1164
  }
@@ -1211,74 +1256,82 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1211
1256
 
1212
1257
  // ---- 2D: sample onto a padded grid, contour it, hand to CrossSection.
1213
1258
  if (is2d) {
1214
- size_t nx = 0, ny = 0;
1215
- double sx = 0, sy = 0;
1216
1259
  if (field) {
1217
1260
  ev.warn("levelset(): a 2D field must be field[i][j]; a 3D array was given",
1218
1261
  &node.position());
1219
1262
  return {};
1220
1263
  }
1264
+ const double x0 = (*bLo)[0], y0 = (*bLo)[1];
1265
+ const double wx = (*bHi)[0] - x0, wy = (*bHi)[1] - y0;
1266
+ double ex = edgeVec ? edgeV[0] : edge, ey = edgeVec ? edgeV[1] : edge;
1267
+ std::optional<std::vector<std::vector<double>>> plane;
1268
+ size_t px = 0, py = 0;
1269
+ bool ownSamples = false;
1221
1270
  if (!fieldFn) {
1222
- const std::optional<std::vector<std::vector<double>>> plane = readPlane(fieldArg);
1271
+ plane = readPlane(fieldArg);
1223
1272
  if (!plane || plane->size() < 2 || (*plane)[0].size() < 2) {
1224
1273
  ev.warn("levelset(): 2D field must be a rectangular field[i][j] of numbers",
1225
1274
  &node.position());
1226
1275
  return {};
1227
1276
  }
1228
- nx = plane->size();
1229
- ny = (*plane)[0].size();
1230
- sx = ((*bHi)[0] - (*bLo)[0]) / static_cast<double>(nx - 1);
1231
- sy = ((*bHi)[1] - (*bLo)[1]) / static_cast<double>(ny - 1);
1232
- Contours2d g;
1233
- g.nx = nx + 2;
1234
- g.ny = ny + 2;
1235
- g.dx = sx;
1236
- g.dy = sy;
1237
- g.ox = (*bLo)[0] - sx;
1238
- g.oy = (*bLo)[1] - sy;
1239
- // Padded with a ring that is firmly OUTSIDE, so a contour meeting
1240
- // the box edge closes along it instead of running off. This is
1241
- // BOSL2's closed=true, and the only behaviour offered.
1242
- g.v.assign(g.nx * g.ny, -1.0);
1243
- for (size_t i = 0; i < nx; ++i)
1244
- for (size_t j = 0; j < ny; ++j)
1245
- g.v[(j + 1) * g.nx + (i + 1)] = bandDistance((*plane)[i][j], isoLo, isoHi, invert);
1246
- const manifold::Polygons polys = marchingSquares(g);
1247
- if (polys.empty()) return {};
1248
- ColoredBody b;
1249
- b.section = clipToBounds(manifold::CrossSection(polys, manifold::CrossSection::FillRule::EvenOdd),
1250
- *bLo, *bHi);
1251
- if (b.section->IsEmpty()) return {};
1252
- std::vector<ColoredBody> out;
1253
- out.push_back(std::move(b));
1254
- return out;
1277
+ px = plane->size();
1278
+ py = (*plane)[0].size();
1279
+ // Contoured on its own samples unless edge= asks for another
1280
+ // spacing, as the 3D grid is marched; then re-sampled bilinearly.
1281
+ const double gx = wx / static_cast<double>(px - 1), gy = wy / static_cast<double>(py - 1);
1282
+ const auto same = [](double a, double b) { return std::abs(a - b) <= 1e-9 * std::max(a, b); };
1283
+ ownSamples = ex <= 0.0 || (same(ex, gx) && same(ey, gy));
1255
1284
  }
1256
- // function(x,y): edge= gives the spacing, as in 3D
1257
- const double ex = edgeVec ? edgeV[0] : edge, ey = edgeVec ? edgeV[1] : edge;
1258
- nx = static_cast<size_t>(std::floor(((*bHi)[0] - (*bLo)[0]) / ex)) + 1;
1259
- ny = static_cast<size_t>(std::floor(((*bHi)[1] - (*bLo)[1]) / ey)) + 1;
1285
+ // Counted from the array, not floor(width / spacing): rounding there
1286
+ // could drop the last sample row.
1287
+ const size_t nx = ownSamples ? px : static_cast<size_t>(std::floor(wx / ex)) + 1;
1288
+ const size_t ny = ownSamples ? py : static_cast<size_t>(std::floor(wy / ey)) + 1;
1260
1289
  if (nx < 2 || ny < 2) {
1261
1290
  ev.warn("levelset(): edge is larger than the bounds", &node.position());
1262
1291
  return {};
1263
1292
  }
1264
- sx = ((*bHi)[0] - (*bLo)[0]) / static_cast<double>(nx - 1);
1265
- sy = ((*bHi)[1] - (*bLo)[1]) / static_cast<double>(ny - 1);
1293
+ const double sx = wx / static_cast<double>(nx - 1), sy = wy / static_cast<double>(ny - 1);
1294
+ // The plane at a point between its samples. Clamped at the edges,
1295
+ // like the 3D grid sampler, so the clip below cuts a clean line.
1296
+ const auto bilinear = [&](double x, double y) {
1297
+ const auto cell = [](double u, size_t n, size_t& i0) {
1298
+ i0 = static_cast<size_t>(std::clamp(std::floor(u), 0.0, static_cast<double>(n - 2)));
1299
+ return std::clamp(u - static_cast<double>(i0), 0.0, 1.0);
1300
+ };
1301
+ size_t i, j;
1302
+ const double tx = cell((x - x0) / wx * static_cast<double>(px - 1), px, i);
1303
+ const double ty = cell((y - y0) / wy * static_cast<double>(py - 1), py, j);
1304
+ const auto& p = *plane;
1305
+ return (1 - tx) * ((1 - ty) * p[i][j] + ty * p[i][j + 1])
1306
+ + tx * ((1 - ty) * p[i + 1][j] + ty * p[i + 1][j + 1]);
1307
+ };
1266
1308
  Contours2d g;
1267
1309
  g.nx = nx + 2;
1268
1310
  g.ny = ny + 2;
1269
1311
  g.dx = sx;
1270
1312
  g.dy = sy;
1271
- g.ox = (*bLo)[0] - sx;
1272
- g.oy = (*bLo)[1] - sy;
1313
+ g.ox = x0 - sx;
1314
+ g.oy = y0 - sy;
1315
+ // Padded with a ring that is firmly OUTSIDE, so a contour meeting
1316
+ // the box edge closes along it instead of running off. This is
1317
+ // BOSL2's closed=true, and the only behaviour offered.
1273
1318
  g.v.assign(g.nx * g.ny, -1.0);
1274
1319
  for (size_t i = 0; i < nx; ++i) {
1275
1320
  for (size_t j = 0; j < ny; ++j) {
1276
- BoundArgs bound;
1277
- bound.set(fnParams2[0], Value{(*bLo)[0] + static_cast<double>(i) * sx});
1278
- bound.set(fnParams2[1], Value{(*bLo)[1] + static_cast<double>(j) * sy});
1279
- const Value outv =
1280
- ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
1281
- const double val = fieldSample(outv).value_or(std::numeric_limits<double>::max());
1321
+ const double x = x0 + static_cast<double>(i) * sx, y = y0 + static_cast<double>(j) * sy;
1322
+ double val;
1323
+ if (ownSamples) {
1324
+ val = (*plane)[i][j];
1325
+ } else if (plane) {
1326
+ val = bilinear(x, y);
1327
+ } else {
1328
+ BoundArgs bound;
1329
+ bound.set(fnParams2[0], Value{x});
1330
+ bound.set(fnParams2[1], Value{y});
1331
+ const Value outv =
1332
+ ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
1333
+ val = fieldSample(outv).value_or(std::numeric_limits<double>::max());
1334
+ }
1282
1335
  g.v[(j + 1) * g.nx + (i + 1)] = bandDistance(val, isoLo, isoHi, invert);
1283
1336
  }
1284
1337
  }
@@ -1360,13 +1413,8 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1360
1413
  if (i0[a] > n[a] - 2) i0[a] = n[a] - 2;
1361
1414
  t[a] = g[a] - static_cast<double>(i0[a]);
1362
1415
  }
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).
1416
+ // Clamped, though every grid node lies inside the block: the
1417
+ // floating-point position of the last node can land a hair past it.
1370
1418
 
1371
1419
  double acc = 0.0;
1372
1420
  for (int c = 0; c < 8; ++c) {
@@ -1378,62 +1426,54 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
1378
1426
  return bandDistance(acc, isoLo, isoHi, invert);
1379
1427
  };
1380
1428
 
1381
- // Manifold closes the mesh where the surface meets the edge of the box it
1382
- // is given, and that closure follows the sample lattice -- a visible
1383
- // staircase on anything that reaches the boundary, which a gyroid does
1384
- // everywhere. So sample a PADDED box, let the ragged closure happen out
1385
- // there, and cut back to the box actually asked for. The cut is then a
1386
- // clean plane.
1429
+ // Every 3D field is marched on a REGULAR grid whose outermost nodes lie
1430
+ // exactly on `bounds`, as BOSL2's isosurface() marches: an array at its
1431
+ // own spacing as is, anything else sampled onto one first (a function by
1432
+ // calling it, an array re-sampled trilinearly).
1387
1433
  //
1388
- // Exactly the fix already applied to the 2D path (clipToBounds); it was
1389
- // not applied here at the time because the 2D error was the one measured.
1390
- const manifold::vec3 E = edgeVec ? manifold::vec3(edgeV[0], edgeV[1], edgeV[2]) : manifold::vec3(edge);
1391
- const manifold::vec3 pad = 2.0 * E;
1392
- manifold::Box bounds(manifold::vec3(origin[0], origin[1], origin[2]) - pad,
1393
- manifold::vec3((*hi)[0], (*hi)[1], (*hi)[2]) + pad);
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.
1434
+ // This replaced Manifold::LevelSet for the function form and for an
1435
+ // array at another spacing (#745 on BelfrySCAD). LevelSet samples its own
1436
+ // body-centred lattice, whose irregular triangles shade as a field of
1437
+ // dimples even where every vertex is on the surface -- worse with a
1438
+ // per-axis edge, which meshed a squashed field and stretched it back. And
1439
+ // that lattice does not land on the box, so where the clip below cut it
1440
+ // the cut face did not follow the same grid as the rest of the surface.
1402
1441
  //
1403
- // canParallel is the crux: safe for a grid because the lambda is pure
1404
- // C++, and NOT safe for a function, which re-enters the evaluator.
1405
- // Manifold: parallel policies "will crash language runtimes with runtime
1406
- // locks that expect to not be called back by unregistered threads".
1407
- //
1408
- // LevelSet takes a single edge length, so a per-axis edge meshes a field
1409
- // squashed to unit spacing on every axis and stretches the result back.
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);
1412
- manifold::Manifold solid;
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;
1442
+ // A per-axis edge that does not divide the box is shrunk to the nearest
1443
+ // one that does, never grown: never coarser than the caller asked.
1444
+ std::array<double, 3> h = spacing;
1445
+ size_t cells[3];
1446
+ for (int a = 0; a < 3; ++a) {
1447
+ const double w = (*hi)[a] - origin[a];
1448
+ if (marchGrid) {
1449
+ cells[a] = static_cast<size_t>(std::llround(w / spacing[a]));
1450
+ } else {
1451
+ const double e = edgeVec ? edgeV[a] : edge;
1452
+ cells[a] = std::max<size_t>(1, static_cast<size_t>(std::ceil(w / e - 1e-9)));
1453
+ h[a] = w / static_cast<double>(cells[a]);
1454
+ }
1426
1455
  }
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) {
1431
- const manifold::Box unit(bounds.min / E, bounds.max / E);
1432
- solid = fieldFn ? manifold::Manifold::LevelSet([&](manifold::vec3 q) { return sampleFn(q * E); },
1433
- unit, 1.0, 0.0, tol, /*canParallel=*/false)
1434
- : manifold::Manifold::LevelSet([&](manifold::vec3 q) { return sampleGrid(q * E); },
1435
- unit, 1.0, 0.0, tol, /*canParallel=*/true);
1436
- solid = solid.Scale(E);
1456
+ // One "outside" node beyond the box on every side, so every surface
1457
+ // closes; the clip below cuts that overhang back to the box.
1458
+ const size_t n[3] = {cells[0] + 3, cells[1] + 3, cells[2] + 3};
1459
+ std::vector<double> d(n[0] * n[1] * n[2], -1.0);
1460
+ for (size_t k = 0; k + 2 < n[2]; ++k)
1461
+ for (size_t j = 0; j + 2 < n[1]; ++j)
1462
+ for (size_t i = 0; i + 2 < n[0]; ++i) {
1463
+ const double x = origin[0] + h[0] * static_cast<double>(i);
1464
+ const double y = origin[1] + h[1] * static_cast<double>(j);
1465
+ const double z = origin[2] + h[2] * static_cast<double>(k);
1466
+ d[((k + 1) * n[1] + (j + 1)) * n[0] + (i + 1)] =
1467
+ marchGrid ? bandDistance(field->at(i, j, k), isoLo, isoHi, invert)
1468
+ : fieldFn ? sampleFn(manifold::vec3(x, y, z))
1469
+ : sampleGrid(manifold::vec3(x, y, z));
1470
+ }
1471
+ const std::array<double, 3> org{origin[0] - h[0], origin[1] - h[1], origin[2] - h[2]};
1472
+ manifold::Manifold solid(marchingCubes(d, n, org, h));
1473
+ if (solid.Status() != manifold::Manifold::Error::NoError) {
1474
+ // Never expected: marchingCubes closes by construction.
1475
+ ev.warn("levelset(): could not build a closed mesh from this field", &node.position());
1476
+ return {};
1437
1477
  }
1438
1478
 
1439
1479
  if (solid.IsEmpty()) return {};
@@ -1374,11 +1374,12 @@ TEST(LevelSetFn, AVectorEdgeOfTheWrongSizeWarns) {
1374
1374
 
1375
1375
  TEST(LevelSetFn, SupportedFeatureSaysAVectorEdgeWorks) {
1376
1376
  // Level 2 is how a library such as BOSL2 tells a build with per-axis
1377
- // edge= from one that would reject it.
1377
+ // edge= from one that would reject it; level 3, one that places a
1378
+ // field array from edge= alone (#749 on BelfrySCAD).
1378
1379
  std::vector<std::string> out;
1379
1380
  evalSrc("echo(supported_feature(\"levelset\"));", [&](const std::string& m) { out.push_back(m); });
1380
1381
  ASSERT_EQ(out.size(), 1u);
1381
- EXPECT_NE(out[0].find("2"), std::string::npos) << out[0];
1382
+ EXPECT_NE(out[0].find("3"), std::string::npos) << out[0];
1382
1383
  }
1383
1384
 
1384
1385
  TEST(LevelSetFn, AFunctionOfTheWrongArityWarns) {
@@ -1605,13 +1606,13 @@ TEST(LevelSet, AGridFieldIsAlsoCutCleanlyAtTheBox) {
1605
1606
  EXPECT_NEAR(capArea, 3.14159265358979 * r * r, 0.03 * 3.14159265358979 * r * r);
1606
1607
  }
1607
1608
 
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.
1609
+ TEST(LevelSetFn, VerticesInterpolateAsBOSL2Does) {
1610
+ // #745 on BelfrySCAD, second round: a function field is sampled onto a
1611
+ // regular grid and marched, as BOSL2's isosurface() does, rather than
1612
+ // meshed by Manifold::LevelSet, whose body-centred lattice shaded as
1613
+ // dimples. Vertices are interpolated linearly along grid edges, so they
1614
+ // are near the surface, not on it: the 1/r field at edge=2 is where that
1615
+ // is worst. A per-axis edge must mesh as well as a plain one.
1615
1616
  for (const char* edge : {"2", "[1,2,0.4]"}) {
1616
1617
  Evaluated e = evalSrc(std::string("levelset(function(x,y,z) 5 / norm([x,y,z]), "
1617
1618
  "bounds=[[-8,-8,-8],[8,8,8]], isovalue=[1, 1/0], edge=") +
@@ -1622,7 +1623,8 @@ TEST(LevelSetFn, VerticesLieOnTheTrueSurface) {
1622
1623
  for (size_t v = 0; v + 2 < m.vertProperties.size(); v += m.numProp)
1623
1624
  worst = std::max(worst, std::abs(manifold::la::length(manifold::vec3(m.vertProperties[v], m.vertProperties[v + 1],
1624
1625
  m.vertProperties[v + 2])) - 5.0));
1625
- EXPECT_LT(worst, 0.03) << edge;
1626
+ EXPECT_LT(worst, 0.3) << edge;
1627
+ EXPECT_EQ(soleBody(e).Genus(), 0) << edge;
1626
1628
  }
1627
1629
  }
1628
1630
 
@@ -1674,6 +1676,95 @@ TEST(LevelSetGrid, TheDefaultMarchesTheGridItself) {
1674
1676
  EXPECT_EQ(soleBody(same).NumTri(), soleBody(e).NumTri());
1675
1677
  }
1676
1678
 
1679
+ TEST(LevelSetGrid, EdgeWithoutBoundsCentresTheGridOnTheOrigin) {
1680
+ // #749 on BelfrySCAD: edge= alone places a field array, its samples
1681
+ // edge apart and centred on the origin, as BOSL2's isosurface() places
1682
+ // an array given voxel_size. Same mesh as the explicit bounds.
1683
+ Evaluated e = evalSrc(sphereFieldSrc(41, 20) + "levelset(f, isovalue=15, edge=1);");
1684
+ Evaluated ref = evalSrc(sphereFieldSrc(41, 20) +
1685
+ "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=15);");
1686
+ ASSERT_EQ(e.bodies.size(), 1u);
1687
+ ASSERT_EQ(ref.bodies.size(), 1u);
1688
+ EXPECT_EQ(soleBody(e).NumTri(), soleBody(ref).NumTri());
1689
+ EXPECT_NEAR(soleBody(e).Volume(), soleBody(ref).Volume(), 1e-9 * soleBody(ref).Volume());
1690
+ EXPECT_GT(onGridLines(soleBody(e), manifold::vec3(-20.0), manifold::vec3(1.0)), 0.5);
1691
+ }
1692
+
1693
+ TEST(LevelSetGrid, AVectorEdgeWithoutBoundsSpacesEachAxis) {
1694
+ // Every sample inside, so the solid is the whole box: 5 x 3 x 4 samples
1695
+ // at [1, 2, 0.5] span 4 x 4 x 1.5, centred.
1696
+ Evaluated e = evalSrc("f = [for (i=[0:4]) [for (j=[0:2]) [for (k=[0:3]) -1 ]]];\n"
1697
+ "levelset(f, edge=[1, 2, 0.5]);");
1698
+ ASSERT_EQ(e.bodies.size(), 1u);
1699
+ const manifold::Box b = soleBody(e).BoundingBox();
1700
+ EXPECT_NEAR(b.min.x, -2.0, 1e-9); EXPECT_NEAR(b.max.x, 2.0, 1e-9);
1701
+ EXPECT_NEAR(b.min.y, -2.0, 1e-9); EXPECT_NEAR(b.max.y, 2.0, 1e-9);
1702
+ EXPECT_NEAR(b.min.z, -0.75, 1e-9); EXPECT_NEAR(b.max.z, 0.75, 1e-9);
1703
+ }
1704
+
1705
+ TEST(LevelSetGrid, AFunctionIsMarchedOnAGridLandingOnTheBounds) {
1706
+ // Per-axis edge [1, 2, 0.4] over a 12 x 16 x 7 box: x and y divide it,
1707
+ // z does not (17.5 cells), so z is shrunk to 7/18 -- never coarsened --
1708
+ // and the grid's last nodes lie exactly on the box. Every vertex is then
1709
+ // on a grid edge.
1710
+ Evaluated e = evalSrc("levelset(function(x,y,z) 5 / norm([x,y,z]), isovalue=[1, 1/0], "
1711
+ "bounds=[[-6,-8,0],[6,8,7]], edge=[1,2,0.4]);");
1712
+ ASSERT_EQ(e.bodies.size(), 1u);
1713
+ EXPECT_GT(onGridLines(soleBody(e), manifold::vec3(-6, -8, 0), manifold::vec3(1, 2, 7.0 / 18.0)), 0.5);
1714
+ }
1715
+
1716
+ TEST(LevelSetGrid, AnArrayAtAnotherEdgeIsResampledOntoAGridLandingOnTheBounds) {
1717
+ // #745 on BelfrySCAD: an array resampled at an edge= not dividing the box
1718
+ // cut faces that did not follow the rest of the surface, since
1719
+ // LevelSet's lattice did not land on the box. 40/2.7 is 14.8 cells, so
1720
+ // the grid is 15 cells of 40/15 and the cut faces are its own.
1721
+ Evaluated e = evalSrc(sphereFieldSrc(41, 20) +
1722
+ "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=25, edge=2.7);");
1723
+ ASSERT_EQ(e.bodies.size(), 1u);
1724
+ EXPECT_GT(onGridLines(soleBody(e), manifold::vec3(-20.0), manifold::vec3(40.0 / 15.0)), 0.5);
1725
+ }
1726
+
1727
+ TEST(LevelSetGrid, A2DFieldArrayIsPlacedByEdgeToo) {
1728
+ // field[i][j] says 2D without a bounds to say so.
1729
+ Evaluated e = evalSrc("f = [for (i=[0:4]) [for (j=[0:2]) -1 ]];\nlevelset(f, edge=2);");
1730
+ ASSERT_EQ(e.bodies.size(), 1u);
1731
+ ASSERT_TRUE(e.bodies[0].section.has_value());
1732
+ const manifold::Rect r = e.bodies[0].section->Bounds();
1733
+ EXPECT_NEAR(r.min.x, -4.0, 1e-9); EXPECT_NEAR(r.max.x, 4.0, 1e-9);
1734
+ EXPECT_NEAR(r.min.y, -2.0, 1e-9); EXPECT_NEAR(r.max.y, 2.0, 1e-9);
1735
+ }
1736
+
1737
+ TEST(LevelSetGrid, A2DGridIsResampledAtADifferentEdge) {
1738
+ // As in 3D: edge= beside bounds re-samples the plane (bilinearly), where
1739
+ // it used to be ignored. A cone sampled every 2, contoured at r=7.
1740
+ const std::string field =
1741
+ "f = [for (i=[0:10]) [for (j=[0:10]) let(x=-10+2*i, y=-10+2*j) sqrt(x*x+y*y) ]];\n";
1742
+ const std::string box = "bounds=[[-10,-10],[10,10]], isovalue=7";
1743
+ Evaluated own = evalSrc(field + "levelset(f, " + box + ");");
1744
+ Evaluated same = evalSrc(field + "levelset(f, " + box + ", edge=2);");
1745
+ Evaluated fine = evalSrc(field + "levelset(f, " + box + ", edge=0.25);");
1746
+ for (Evaluated* e : {&own, &same, &fine}) {
1747
+ ASSERT_EQ(e->bodies.size(), 1u);
1748
+ ASSERT_TRUE(e->bodies[0].section.has_value());
1749
+ }
1750
+ EXPECT_EQ(same.bodies[0].section->NumVert(), own.bodies[0].section->NumVert());
1751
+ EXPECT_GT(fine.bodies[0].section->NumVert(), 4 * own.bodies[0].section->NumVert());
1752
+ const double disc = 3.14159265358979 * 49;
1753
+ EXPECT_NEAR(fine.bodies[0].section->Area(), disc, 0.03 * disc);
1754
+ }
1755
+
1756
+ TEST(LevelSetGrid, NeitherBoundsNorEdgeSaysWhatIsMissing) {
1757
+ const std::vector<std::string> w =
1758
+ levelsetWarnings("f = [for (i=[0:4]) [for (j=[0:2]) [for (k=[0:3]) -1 ]]];\nlevelset(f);");
1759
+ ASSERT_EQ(w.size(), 1u);
1760
+ EXPECT_NE(w[0].find("bounds=, or edge="), std::string::npos) << w[0];
1761
+ // A function field has no sizes to infer from: bounds stays required.
1762
+ const std::vector<std::string> fw =
1763
+ levelsetWarnings("levelset(function(x,y,z) x, edge=1);");
1764
+ ASSERT_EQ(fw.size(), 1u);
1765
+ EXPECT_NE(fw[0].find("bounds must be"), std::string::npos) << fw[0];
1766
+ }
1767
+
1677
1768
  TEST(LevelSetGrid, ADifferentEdgeStillResamples) {
1678
1769
  Evaluated e = evalSrc(sphereFieldSrc(41, 20) +
1679
1770
  "levelset(f, bounds=[[-20,-20,-20],[20,20,20]], isovalue=15, edge=0.7);");
@@ -55,7 +55,7 @@ wheels = [
55
55
 
56
56
  [[package]]
57
57
  name = "openscad-cpp-evaluator"
58
- version = "1.47.0"
58
+ version = "1.49.0"
59
59
  source = { editable = "." }
60
60
  dependencies = [
61
61
  { name = "numpy" },