openscad-cpp-evaluator 0.49.0__tar.gz → 0.50.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/CLAUDE.md +45 -0
  2. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/builtins.hpp +5 -0
  5. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/function_builtins.cpp +14 -3
  6. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/registry.cpp +3 -0
  7. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/topology.cpp +255 -0
  8. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_booleans.cpp +227 -0
  9. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_feature_detection.cpp +1 -1
  10. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_function_builtins.cpp +14 -0
  11. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.github/workflows/ci.yml +0 -0
  12. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.github/workflows/wheels.yml +0 -0
  13. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.gitignore +0 -0
  14. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.gitmodules +0 -0
  15. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/CMakeLists.txt +0 -0
  16. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/LICENSE +0 -0
  17. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/README.md +0 -0
  18. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/bindings/CMakeLists.txt +0 -0
  19. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/bindings/ast_to_py.cpp +0 -0
  20. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/bindings/module.cpp +0 -0
  21. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/cmake/embed_font.cmake +0 -0
  22. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/CMakeLists.txt +0 -0
  23. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/eval_perf_benchmark.cpp +0 -0
  24. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/manifold_cache_reuse.cpp +0 -0
  25. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/minimal_debugger.cpp +0 -0
  26. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  27. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/.gitignore +0 -0
  28. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  29. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  30. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/README.md +0 -0
  31. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  32. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  33. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  34. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  35. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  36. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  37. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  38. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  39. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  40. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  41. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  42. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  43. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  44. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  45. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  46. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  47. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/api.cpp +0 -0
  48. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  49. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  50. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  51. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  52. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  53. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  54. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  55. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  56. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  57. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  58. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  59. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  60. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  61. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  62. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  63. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  64. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/position.cpp +0 -0
  65. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  66. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  67. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  68. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  69. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  70. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  71. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  72. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  73. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  74. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  75. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  76. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  77. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  78. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  79. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  80. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  81. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  82. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  83. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  84. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  85. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  86. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  87. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  88. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  89. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  90. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  91. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  92. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  93. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  94. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  95. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  96. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  97. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  98. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  99. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  100. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  101. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  102. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  103. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  104. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  105. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  106. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  107. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  108. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  109. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/export.hpp +0 -0
  110. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  111. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  112. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  113. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  114. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  115. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  116. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  117. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  118. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  119. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  120. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  121. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  122. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  123. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  124. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/value.hpp +0 -0
  125. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  126. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/python/openscad_cpp_evaluator/__init__.py +0 -0
  127. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/resources/fonts/LICENSE +0 -0
  128. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  129. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/CMakeLists.txt +0 -0
  130. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/booleans.cpp +0 -0
  131. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/call_args.cpp +0 -0
  132. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/color.cpp +0 -0
  133. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/control.cpp +0 -0
  134. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/dxf_dim.cpp +0 -0
  135. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/extrude.cpp +0 -0
  136. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/import.cpp +0 -0
  137. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/modifiers.cpp +0 -0
  138. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/primitives_2d.cpp +0 -0
  139. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/primitives_3d.cpp +0 -0
  140. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/roof.cpp +0 -0
  141. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/segments.cpp +0 -0
  142. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/surface.cpp +0 -0
  143. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/text.cpp +0 -0
  144. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/transforms.cpp +0 -0
  145. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/bytecode_compiler.cpp +0 -0
  146. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/bytecode_vm.cpp +0 -0
  147. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/colored_body.cpp +0 -0
  148. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/csg_generate.cpp +0 -0
  149. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/csg_resolve.cpp +0 -0
  150. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/css_colors.cpp +0 -0
  151. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/debug/debug_repl.cpp +0 -0
  152. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/debug_profile.cpp +0 -0
  153. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/eval_context.cpp +0 -0
  154. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/eval_error.cpp +0 -0
  155. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/eval_use.cpp +0 -0
  156. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/export.cpp +0 -0
  157. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/expr_eval.cpp +0 -0
  158. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/dxf_import.cpp +0 -0
  159. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/mesh_import.cpp +0 -0
  160. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/surface_load.cpp +0 -0
  161. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/svg_import.cpp +0 -0
  162. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/manifold_cache.cpp +0 -0
  163. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/measure_geometry.cpp +0 -0
  164. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/mesh_check.cpp +0 -0
  165. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/stmt_eval.cpp +0 -0
  166. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/text/font_match.cpp +0 -0
  167. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/text/freetype_font_provider.cpp +0 -0
  168. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/text/text_metrics.cpp +0 -0
  169. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/user_calls.cpp +0 -0
  170. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/value.cpp +0 -0
  171. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/zip_stored.cpp +0 -0
  172. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/CMakeLists.txt +0 -0
  173. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_bytecode_compiler.cpp +0 -0
  174. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_children_separate.cpp +0 -0
  175. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_cli.cpp +0 -0
  176. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_control_flow.cpp +0 -0
  177. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_csg_tree.cpp +0 -0
  178. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_debug_hooks.cpp +0 -0
  179. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_dxf_svg_import.cpp +0 -0
  180. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_eval_context.cpp +0 -0
  181. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_export.cpp +0 -0
  182. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_expr_eval.cpp +0 -0
  183. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_extrude_roof.cpp +0 -0
  184. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_helpers.hpp +0 -0
  185. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_import_export.cpp +0 -0
  186. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_manifold_cache.cpp +0 -0
  187. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_mesh_check.cpp +0 -0
  188. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_multi_color_merge.cpp +0 -0
  189. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_primitives.cpp +0 -0
  190. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_profiling.cpp +0 -0
  191. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_python_bindings.py +0 -0
  192. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_render_expr.cpp +0 -0
  193. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_scope_trail.cpp +0 -0
  194. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_scoping.cpp +0 -0
  195. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_surface.cpp +0 -0
  196. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_tail_calls.cpp +0 -0
  197. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_text.cpp +0 -0
  198. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_transforms.cpp +0 -0
  199. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_unexpected_args.cpp +0 -0
  200. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_use.cpp +0 -0
  201. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/uv.lock +0 -0
@@ -460,6 +460,51 @@ grep for `ponytail:`.
460
460
  unknown arguments -- `children(separate=true)` renders the wrong shape there with no warning --
461
461
  and both read as `undef` in OpenSCAD, so a guard written against them works in both.
462
462
 
463
+ - **`levelset(field, bounds, isovalue)`** (`src/builtins/topology.cpp`, feature name `levelset`) —
464
+ a solid from a grid of sampled values: `field[i][j][k]` with i,j,k mapping to x,y,z. Wraps
465
+ `Manifold::LevelSet` with a C++ trilinear-sampling lambda; no new Manifold API.
466
+ **The field is a grid OR a `function(x,y,z)`, and the two trade off against each other** —
467
+ neither is simply better:
468
+
469
+ | | sampling | parallel | accuracy | ceiling |
470
+ |---|---|---|---|---|
471
+ | grid `field[i][j][k]` | 0.41 µs | **yes** | capped by the grid | ~550 MB at 200³ |
472
+ | `function(x,y,z)` | 0.96 µs | **no** | Manifold snaps toward the true surface | none |
473
+
474
+ Measured at matched resolution against the analytic sphere: **50³ grid 0.23 s (−0.244%) vs
475
+ function 0.32 s (−0.103%); 100³ grid 1.06 s (−0.059%) vs function 1.91 s (−0.025%)**. So the
476
+ function form is ~1.5× slower and ~2.4× more accurate. Both still beat BOSL2.
477
+ `canParallel` is the crux and is **not a tuning knob**: true for a grid because the lambda is
478
+ pure C++, false for a function because Manifold warns that parallel policies "will crash
479
+ language runtimes with runtime locks that expect to not be called back by unregistered threads".
480
+ The function form also needs `edge=` explicitly — there is no grid to infer spacing from and the
481
+ cost is cubic in it, so guessing would silently produce either a useless mesh or a ten-minute
482
+ one.
483
+ Measured against BOSL2's `isosurface.scad` (217 KB of marching cubes in script), same field:
484
+ **50³ 0.26 s vs 2.07 s (8.0×), 100³ 1.15 s vs 14.59 s (12.7×)**.
485
+ The trade is **memory**: ~67 bytes/cell, so 200³ ≈ 550 MB is the practical ceiling. A callback
486
+ samples lazily and has none.
487
+ **Accuracy is bounded by the grid, not by Manifold.** It samples on a body-centred cubic lattice
488
+ (`sdf.cpp:440-446`) so a plain cubic grid does not line up and the lambda interpolates — same
489
+ accuracy as grid-based marching cubes in script, worse than `LevelSet` given a true SDF. Hence
490
+ `tolerance = -1` is forced: a positive value makes Manifold do extra evaluations per vertex that
491
+ would only re-interpolate data already used. `edgeLength` defaults to the grid spacing for the
492
+ same reason.
493
+ Calling the field function from the generate pass has no live `EvalContext`, so a root is built
494
+ from the closure's own scope — `$`-variables sit at their defaults inside it.
495
+ **An explicitly-undef optional argument counts as ABSENT**, not as a bad one — the same rule in
496
+ `linear_solve` (`b`) and `levelset` (`isovalue`). BOSL2 reaches a builtin it also defines by
497
+ wrapping it in `builtins.scad`, which `use <>`s nothing, so the names there bind to the builtins:
498
+ `function _linear_solve(A, b) = linear_solve(A, b);`. That wrapper has a fixed signature and so
499
+ forwards `b` even when the caller passed none, so treating undef as a *bad* argument warned on
500
+ every `determinant()`-style call. Passing undef is idiomatically the same as not passing in
501
+ OpenSCAD, which is how BOSL2 threads optional arguments through wrappers throughout.
502
+ Verified against OpenSCAD 2026.02.01: the wrapper pattern resolves identically there.
503
+ Sign convention: Manifold takes positive as inside; a distance field is the other way round, so
504
+ the default flips it and `invert=true` flips back.
505
+ Two behaviours that look alike and are opposite: an isovalue **nothing reaches** gives empty
506
+ geometry; one **everything satisfies** gives the whole bounding box. Both are tested.
507
+
463
508
  - **`linear_solve(A, b)`** (`src/builtins/function_builtins.cpp`, feature name `linear-solve`) —
464
509
  returns `.x` (solution), `.det`, `.singular`. Dispatches on shape:
465
510
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.49.0
3
+ Version: 0.50.1
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.49.0"
7
+ version = "0.50.1"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -183,6 +183,11 @@ std::vector<ColoredBody> generateHull(Evaluator& ev, const CSGParams& params,
183
183
  Value builtinDxfDim(Evaluator& ev, const CallArgs& args, const oscad::ASTNode& node);
184
184
  Value builtinDxfCross(Evaluator& ev, const CallArgs& args, const oscad::ASTNode& node);
185
185
 
186
+ CSGParams resolveLevelSet(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
187
+ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params,
188
+ const std::vector<std::unique_ptr<CSGNode>>& children,
189
+ const oscad::ASTNode& node);
190
+
186
191
  CSGParams resolveSimplify(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
187
192
  std::vector<ColoredBody> generateSimplify(Evaluator& ev, const CSGParams& params,
188
193
  const std::vector<std::unique_ptr<CSGNode>>& children,
@@ -913,6 +913,7 @@ const std::unordered_map<std::string, double>& featureLevels() {
913
913
  static const std::unordered_map<std::string, double> levels = {
914
914
  {"render-expr", 1.0}, // render() in expression position
915
915
  {"linear-solve", 1.0}, // linear_solve(A, b) -> {x, det, singular}
916
+ {"levelset", 1.0}, // levelset(field, bounds, isovalue)
916
917
  {"polyhedron-vnf", 1.0}, // polyhedron(vnf) / polyhedron(object)
917
918
  {"separate-children", 1.0}, // children(..., separate=true)
918
919
  {"minkowski-diff", 1.0}, // minkowski_difference()
@@ -1344,9 +1345,19 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
1344
1345
  // The OpenSCAD release we track. version_num() is that same
1345
1346
  // year/month/day folded as y * 10000 + m * 100 + d, exactly like the
1346
1347
  // reference's own builtin_version_num (builtin_functions.cc).
1347
- case BuiltinFnId::LinearSolve:
1348
- return builtinLinearSolve(ev, getArg(args, 0, "A", Value{}), getArg(args, 1, "b", Value{}),
1349
- args.findPositional(1) != nullptr || args.findNamed("b") != nullptr, node);
1348
+ case BuiltinFnId::LinearSolve: {
1349
+ // An explicitly-undef b counts as ABSENT, not as a bad argument.
1350
+ // A wrapper with a fixed signature -- BOSL2's builtins.scad
1351
+ // pattern, `function _linear_solve(A, b) = linear_solve(A, b);`
1352
+ // -- always forwards b, so determinant()-style callers that pass
1353
+ // no right-hand side would otherwise warn on every call.
1354
+ // Passing undef is idiomatically the same as not passing in
1355
+ // OpenSCAD, which is how optional arguments are threaded through
1356
+ // wrappers throughout BOSL2.
1357
+ const Value bArg = getArg(args, 1, "b", Value{});
1358
+ const bool haveB = !std::holds_alternative<std::monostate>(bArg);
1359
+ return builtinLinearSolve(ev, getArg(args, 0, "A", Value{}), bArg, haveB, node);
1360
+ }
1350
1361
  case BuiltinFnId::SupportedFeature: {
1351
1362
  const Value name = getArg(args, 0, "feature", Value{});
1352
1363
  const std::string* s = std::get_if<std::string>(&name);
@@ -37,6 +37,7 @@ const std::unordered_map<std::string_view, ResolveFn>& resolveDispatch() {
37
37
  {"fill", &resolveFill},
38
38
  {"minkowski_difference", &resolveMinkowskiDifference},
39
39
  {"simplify", &resolveSimplify},
40
+ {"levelset", &resolveLevelSet},
40
41
  {"offset", &resolveOffset},
41
42
  {"surface", &resolveSurface},
42
43
  {"text", &resolveText},
@@ -78,6 +79,7 @@ const std::unordered_map<std::string_view, GenerateFn>& generateDispatch() {
78
79
  {"fill", &generateFill},
79
80
  {"minkowski_difference", &generateMinkowskiDifference},
80
81
  {"simplify", &generateSimplify},
82
+ {"levelset", &generateLevelSet},
81
83
  {"offset", &generateOffset},
82
84
  {"surface", &generateSurface},
83
85
  {"text", &generateText},
@@ -125,6 +127,7 @@ const std::vector<std::string>* builtinParamNames(const std::string& name) {
125
127
  {"fill", {}},
126
128
  {"minkowski_difference", {}},
127
129
  {"simplify", {"tolerance"}},
130
+ {"levelset", {"field", "bounds", "isovalue", "invert", "edge"}},
128
131
  {"offset", {"r", "delta", "chamfer"}},
129
132
  {"surface", {"file", "center", "convexity", "invert"}},
130
133
  {"text", {"text", "size", "font", "direction", "language", "script", "halign", "valign", "spacing"}},
@@ -451,4 +451,259 @@ std::vector<ColoredBody> generateSimplify(Evaluator& ev, const CSGParams& params
451
451
  return out;
452
452
  }
453
453
 
454
+ // -- levelset(field, bounds, isovalue) -------------------------------------
455
+ //
456
+ // A solid from a grid of sampled values. Wraps Manifold::LevelSet with a C++
457
+ // trilinear-sampling lambda over the array the script handed us.
458
+ //
459
+ // Why a grid rather than Manifold's own SDF-callback shape, which would be
460
+ // the obvious mapping: measured, building the field in OpenSCAD costs
461
+ // 0.41 us/sample against 0.96 us for a closure call per sample, because the
462
+ // arithmetic inlines into the list comprehension. More importantly a grid
463
+ // means the C++ side never re-enters the evaluator, so canParallel can be
464
+ // TRUE -- Manifold's docs warn that parallel policies "will crash language
465
+ // runtimes with runtime locks that expect to not be called back by
466
+ // unregistered threads", which is exactly what a callback design forfeits.
467
+ //
468
+ // The trade is memory: ~67 bytes per cell, so 200^3 is about 550 MB and that
469
+ // is the practical ceiling. A callback samples lazily and has no ceiling.
470
+ //
471
+ // Accuracy is bounded by the GRID, not by Manifold. Manifold samples on a
472
+ // body-centred cubic lattice (two interleaved cubic grids, sdf.cpp:440-446)
473
+ // and a plain cubic grid does not line up with those points, so the lambda
474
+ // interpolates. Same accuracy as a grid-based marching-cubes implementation
475
+ // in script; worse than LevelSet given a true SDF. Do not imply otherwise.
476
+
477
+ namespace {
478
+
479
+ struct ScalarField {
480
+ std::vector<double> v; // flat, x fastest
481
+ size_t nx = 0, ny = 0, nz = 0;
482
+ double at(size_t i, size_t j, size_t k) const { return v[(k * ny + j) * nx + i]; }
483
+ };
484
+
485
+ // field[i][j][k] -> flat, checking it is a full rectangular block.
486
+ std::optional<ScalarField> readField(const Value& val) {
487
+ const ListPtr* xs = std::get_if<ListPtr>(&val);
488
+ if (!xs || !*xs || (*xs)->items.empty()) return std::nullopt;
489
+ ScalarField f;
490
+ f.nx = (*xs)->items.size();
491
+ for (size_t i = 0; i < f.nx; ++i) {
492
+ const ListPtr* ys = std::get_if<ListPtr>(&(*xs)->items[i]);
493
+ if (!ys || !*ys || (*ys)->items.empty()) return std::nullopt;
494
+ if (i == 0) f.ny = (*ys)->items.size();
495
+ else if ((*ys)->items.size() != f.ny) return std::nullopt;
496
+ for (size_t j = 0; j < f.ny; ++j) {
497
+ const ListPtr* zs = std::get_if<ListPtr>(&(*ys)->items[j]);
498
+ if (!zs || !*zs || (*zs)->items.empty()) return std::nullopt;
499
+ if (i == 0 && j == 0) {
500
+ f.nz = (*zs)->items.size();
501
+ f.v.assign(f.nx * f.ny * f.nz, 0.0);
502
+ } else if ((*zs)->items.size() != f.nz) {
503
+ return std::nullopt;
504
+ }
505
+ for (size_t k = 0; k < f.nz; ++k) {
506
+ const double* d = std::get_if<double>(&(*zs)->items[k]);
507
+ if (!d || !std::isfinite(*d)) return std::nullopt;
508
+ f.v[(k * f.ny + j) * f.nx + i] = *d;
509
+ }
510
+ }
511
+ }
512
+ return f;
513
+ }
514
+
515
+ std::optional<std::array<double, 3>> readVec3(const Value& v) {
516
+ const ListPtr* l = std::get_if<ListPtr>(&v);
517
+ if (!l || !*l || (*l)->items.size() != 3) return std::nullopt;
518
+ std::array<double, 3> out{};
519
+ for (size_t i = 0; i < 3; ++i) {
520
+ const double* d = std::get_if<double>(&(*l)->items[i]);
521
+ if (!d || !std::isfinite(*d)) return std::nullopt;
522
+ out[i] = *d;
523
+ }
524
+ return out;
525
+ }
526
+
527
+ } // namespace
528
+
529
+ CSGParams resolveLevelSet(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
530
+ auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
531
+ CSGParams params;
532
+ params["field"] = getArg(args, 0, "field", Value{});
533
+ params["bounds"] = getArg(args, 1, "bounds", Value{});
534
+ params["isovalue"] = getArg(args, 2, "isovalue", Value{0.0});
535
+ params["invert"] = getArg(args, 3, "invert", Value{false});
536
+ params["edge"] = getArg(args, 4, "edge", Value{});
537
+ ev.evalChildren(node.children, effCtx);
538
+ return params;
539
+ }
540
+
541
+ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params,
542
+ const std::vector<std::unique_ptr<CSGNode>>&,
543
+ const oscad::ASTNode& node) {
544
+ // Two shapes of field, and the choice matters beyond taste:
545
+ //
546
+ // a GRID -- faster to sample (0.41 vs 0.96 us) and meshes in
547
+ // PARALLEL, because nothing re-enters the evaluator.
548
+ // Accuracy is capped by the grid; memory is the ceiling.
549
+ // a FUNCTION -- Manifold picks its own sample points and can snap to
550
+ // the true surface, so it is MORE accurate, with no
551
+ // memory ceiling. But every sample is a closure call, and
552
+ // canParallel must be false or the evaluator gets called
553
+ // back from threads it knows nothing about.
554
+ const Value& fieldArg = params.at("field");
555
+ const ClosurePtr* fieldFn = std::get_if<ClosurePtr>(&fieldArg);
556
+ std::optional<ScalarField> field;
557
+ if (!fieldFn) {
558
+ field = readField(fieldArg);
559
+ if (!field) {
560
+ ev.warn("levelset(): field must be a function(x,y,z) or a rectangular field[i][j][k] of numbers",
561
+ &node.position());
562
+ return {};
563
+ }
564
+ if (field->nx < 2 || field->ny < 2 || field->nz < 2) {
565
+ ev.warn("levelset(): the field needs at least 2 samples along each axis", &node.position());
566
+ return {};
567
+ }
568
+ } else if (!*fieldFn || (*fieldFn)->node == nullptr || (*fieldFn)->node->parameters.size() < 3) {
569
+ ev.warn("levelset(): the field function needs three parameters, as in function(x,y,z) ...",
570
+ &node.position());
571
+ return {};
572
+ }
573
+
574
+ const ListPtr* bb = std::get_if<ListPtr>(&params.at("bounds"));
575
+ std::optional<std::array<double, 3>> lo, hi;
576
+ if (bb && *bb && (*bb)->items.size() == 2) {
577
+ lo = readVec3((*bb)->items[0]);
578
+ hi = readVec3((*bb)->items[1]);
579
+ }
580
+ if (!lo || !hi) {
581
+ ev.warn("levelset(): bounds must be [[x0,y0,z0],[x1,y1,z1]]", &node.position());
582
+ return {};
583
+ }
584
+ for (int a = 0; a < 3; ++a) {
585
+ if (!((*hi)[a] > (*lo)[a])) {
586
+ ev.warn("levelset(): bounds must be increasing along every axis", &node.position());
587
+ return {};
588
+ }
589
+ }
590
+
591
+ // Same rule as linear_solve's b: an explicitly-undef argument is ABSENT,
592
+ // so a fixed-signature wrapper forwarding every parameter still works.
593
+ const Value& isoVal = params.at("isovalue");
594
+ double isovalue = 0.0;
595
+ if (!std::holds_alternative<std::monostate>(isoVal)) {
596
+ const double* isoArg = std::get_if<double>(&isoVal);
597
+ if (!isoArg) {
598
+ ev.warn("levelset(): isovalue must be a number", &node.position());
599
+ return {};
600
+ }
601
+ isovalue = *isoArg;
602
+ }
603
+ const bool invert = truthy(params.at("invert"));
604
+
605
+ const std::array<double, 3> origin = *lo;
606
+ std::array<double, 3> spacing{1.0, 1.0, 1.0};
607
+ double edge = 0.0;
608
+ if (field) {
609
+ // Grid spacing is corner-sample to corner-sample, so n-1 intervals.
610
+ spacing = {((*hi)[0] - (*lo)[0]) / static_cast<double>(field->nx - 1),
611
+ ((*hi)[1] - (*lo)[1]) / static_cast<double>(field->ny - 1),
612
+ ((*hi)[2] - (*lo)[2]) / static_cast<double>(field->nz - 1)};
613
+ // Finer than the grid buys nothing -- there is no information between
614
+ // samples -- and coarser throws away what the script paid to compute.
615
+ edge = std::min({spacing[0], spacing[1], spacing[2]});
616
+ }
617
+ if (const double* e = std::get_if<double>(&params.at("edge"))) {
618
+ if (*e > 0.0) edge = *e;
619
+ else ev.warn("levelset(): edge must be positive", &node.position());
620
+ }
621
+ if (edge <= 0.0) {
622
+ // No grid to infer it from. Guessing would silently pick either a
623
+ // useless mesh or a ten-minute one -- the cost is cubic in this
624
+ // number, so it is the caller's call to make.
625
+ ev.warn("levelset(): a function field needs edge= (the sample spacing)", &node.position());
626
+ return {};
627
+ }
628
+
629
+ // The function form: one closure call per sample. No live EvalContext at
630
+ // generate time, so a root is built from the closure's own scope -- the
631
+ // same approach the warp experiment used. $-variables are therefore at
632
+ // their defaults inside a field function.
633
+ std::optional<EvalContext> fnCtx;
634
+ std::array<std::string, 3> fnParams;
635
+ if (fieldFn) {
636
+ fnCtx = EvalContext::makeRoot((*fieldFn)->node->scope());
637
+ for (int a = 0; a < 3; ++a) fnParams[static_cast<size_t>(a)] = (*fieldFn)->node->parameters[static_cast<size_t>(a)]->name->name;
638
+ }
639
+
640
+ const auto sampleFn = [&](manifold::vec3 p) -> double {
641
+ BoundArgs bound;
642
+ const double xyz[3] = {p.x, p.y, p.z};
643
+ for (int a = 0; a < 3; ++a) bound.set(fnParams[static_cast<size_t>(a)], Value{xyz[a]});
644
+ const Value out = ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
645
+ const double* d = std::get_if<double>(&out);
646
+ // A field that returns junk at one point must not abort the whole
647
+ // build; treat it as far outside instead.
648
+ const double v = (d && std::isfinite(*d)) ? *d : std::numeric_limits<double>::max();
649
+ const double diff = v - isovalue;
650
+ return invert ? diff : -diff;
651
+ };
652
+
653
+ // Never dereferenced on the function path, but it must still be a valid
654
+ // reference -- binding one to a null pointer is undefined behaviour even
655
+ // where the lambda is never called.
656
+ const ScalarField emptyField;
657
+ const ScalarField& f = field ? *field : emptyField;
658
+ const auto sampleGrid = [&f, origin, spacing, isovalue, invert](manifold::vec3 p) -> double {
659
+ double g[3];
660
+ size_t i0[3];
661
+ double t[3];
662
+ const double pos[3] = {p.x, p.y, p.z};
663
+ const size_t n[3] = {f.nx, f.ny, f.nz};
664
+ for (int a = 0; a < 3; ++a) {
665
+ g[a] = (pos[a] - origin[a]) / spacing[a];
666
+ if (g[a] < 0.0) g[a] = 0.0;
667
+ const double maxg = static_cast<double>(n[a] - 1);
668
+ if (g[a] > maxg) g[a] = maxg;
669
+ i0[a] = static_cast<size_t>(g[a]);
670
+ if (i0[a] > n[a] - 2) i0[a] = n[a] - 2;
671
+ t[a] = g[a] - static_cast<double>(i0[a]);
672
+ }
673
+ double acc = 0.0;
674
+ for (int c = 0; c < 8; ++c) {
675
+ const size_t di = static_cast<size_t>(c & 1), dj = static_cast<size_t>((c >> 1) & 1),
676
+ dk = static_cast<size_t>((c >> 2) & 1);
677
+ const double w = (di ? t[0] : 1.0 - t[0]) * (dj ? t[1] : 1.0 - t[1]) * (dk ? t[2] : 1.0 - t[2]);
678
+ acc += w * f.at(i0[0] + di, i0[1] + dj, i0[2] + dk);
679
+ }
680
+ // Manifold takes POSITIVE as inside. A distance field is the other
681
+ // way round, which is the common case, so the default flips it.
682
+ const double d = acc - isovalue;
683
+ return invert ? d : -d;
684
+ };
685
+
686
+ manifold::Box bounds(manifold::vec3(origin[0], origin[1], origin[2]),
687
+ manifold::vec3((*hi)[0], (*hi)[1], (*hi)[2]));
688
+ // tolerance -1: a positive value makes Manifold do EXTRA evaluations per
689
+ // output vertex to snap nearer the true surface. Against a fixed grid
690
+ // those only re-interpolate data already used -- cost, no information.
691
+ // canParallel true: the lambda is pure C++ and never re-enters the
692
+ // evaluator, which is the whole point of taking a grid.
693
+ // canParallel is the crux: safe for a grid because the lambda is pure
694
+ // C++, and NOT safe for a function, which re-enters the evaluator.
695
+ // Manifold: parallel policies "will crash language runtimes with runtime
696
+ // locks that expect to not be called back by unregistered threads".
697
+ manifold::Manifold solid =
698
+ fieldFn ? manifold::Manifold::LevelSet(sampleFn, bounds, edge, 0.0, -1.0, /*canParallel=*/false)
699
+ : manifold::Manifold::LevelSet(sampleGrid, bounds, edge, 0.0, -1.0, /*canParallel=*/true);
700
+
701
+ if (solid.IsEmpty()) return {};
702
+ ColoredBody b;
703
+ b.body = std::move(solid);
704
+ std::vector<ColoredBody> out;
705
+ out.push_back(std::move(b));
706
+ return out;
707
+ }
708
+
454
709
  } // namespace oscadeval
@@ -793,3 +793,230 @@ TEST(Simplify, NoChildrenIsEmpty) {
793
793
  Evaluated e = evalSrc("simplify();");
794
794
  EXPECT_TRUE(e.bodies.empty());
795
795
  }
796
+
797
+ // -- levelset -------------------------------------------------------------
798
+ //
799
+ // A solid from a grid of sampled values. Takes a grid rather than an SDF
800
+ // callback: measured, building the field in script costs 0.41 us/sample
801
+ // against 0.96 us for a closure call, and a grid lets Manifold mesh in
802
+ // parallel because nothing re-enters the evaluator.
803
+ //
804
+ // Accuracy is bounded by the grid, not by Manifold -- it samples on a
805
+ // body-centred cubic lattice and the lambda interpolates -- so every
806
+ // tolerance here is a percentage, not an epsilon.
807
+
808
+ namespace {
809
+
810
+ // field[i][j][k] of the distance from the origin, over [-half, half]^3.
811
+ std::string sphereFieldSrc(int n, double half) {
812
+ return "N = " + std::to_string(n) + "; H = " + std::to_string(half) +
813
+ ";\nfunction co(t) = -H + 2*H*t/(N-1);\n"
814
+ "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
815
+ " sqrt(co(i)*co(i) + co(j)*co(j) + co(k)*co(k)) ]]];\n";
816
+ }
817
+
818
+ std::vector<std::string> levelsetWarnings(const std::string& code) {
819
+ std::vector<std::string> out;
820
+ evalSrc(code, [&](const std::string& m) {
821
+ if (m.rfind("WARNING", 0) == 0) out.push_back(m);
822
+ });
823
+ return out;
824
+ }
825
+
826
+ } // namespace
827
+
828
+ TEST(LevelSet, ASphereFieldGivesASphere) {
829
+ Evaluated e = evalSrc(sphereFieldSrc(50, 30) +
830
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
831
+ ASSERT_EQ(e.bodies.size(), 1u);
832
+ const double analytic = 4.0 / 3.0 * 3.14159265358979 * 20 * 20 * 20;
833
+ EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.01 * analytic); // within 1%
834
+ EXPECT_EQ(soleBody(e).Genus(), 0);
835
+ }
836
+
837
+ TEST(LevelSet, AccuracyImprovesWithGridResolution) {
838
+ // The grid is the accuracy limit, so more samples must mean less error.
839
+ // Anything else means the resolution argument is not doing its job.
840
+ const double analytic = 4.0 / 3.0 * 3.14159265358979 * 20 * 20 * 20;
841
+ double prevErr = 1e30;
842
+ for (int n : {25, 40, 60}) {
843
+ Evaluated e = evalSrc(sphereFieldSrc(n, 30) +
844
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
845
+ ASSERT_EQ(e.bodies.size(), 1u) << "n=" << n;
846
+ const double err = std::abs(soleBody(e).Volume() - analytic);
847
+ EXPECT_LT(err, prevErr) << "n=" << n << " was no better than the coarser grid";
848
+ prevErr = err;
849
+ }
850
+ }
851
+
852
+ TEST(LevelSet, ATorusFieldHasGenusOne) {
853
+ // The test that catches an implementation producing plausible-looking
854
+ // but topologically wrong output -- volume alone would not.
855
+ Evaluated e = evalSrc(
856
+ "N = 60; H = 30;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
857
+ "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
858
+ " let(q = sqrt(co(i)*co(i) + co(j)*co(j)) - 15)\n"
859
+ " sqrt(q*q + co(k)*co(k)) ]]];\n"
860
+ "levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=6);");
861
+ ASSERT_EQ(e.bodies.size(), 1u);
862
+ EXPECT_EQ(soleBody(e).Genus(), 1);
863
+ }
864
+
865
+ TEST(LevelSet, DisjointBlobsComeOutAsSeparateComponents) {
866
+ // Two spheres: Euler characteristic 4, so genus 1 - 4/2 = -1.
867
+ Evaluated e = evalSrc(
868
+ "N = 60; H = 30;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
869
+ "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
870
+ " min(sqrt((co(i)-12)*(co(i)-12) + co(j)*co(j) + co(k)*co(k)),\n"
871
+ " sqrt((co(i)+12)*(co(i)+12) + co(j)*co(j) + co(k)*co(k))) ]]];\n"
872
+ "levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=8);");
873
+ ASSERT_EQ(e.bodies.size(), 1u);
874
+ EXPECT_EQ(soleBody(e).Genus(), -1);
875
+ }
876
+
877
+ TEST(LevelSet, IndexOrderIsXThenYThenZ) {
878
+ // Deliberately asymmetric: a symmetric field passes even if the axes are
879
+ // transposed, which is the classic marching-cubes bug.
880
+ Evaluated e = evalSrc(
881
+ "N = 60; H = 30;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
882
+ "f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
883
+ " max(abs(co(i))/5, abs(co(j))/10, abs(co(k))/20) ]]];\n"
884
+ "levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=1);");
885
+ ASSERT_EQ(e.bodies.size(), 1u);
886
+ const manifold::Box b = soleBody(e).BoundingBox();
887
+ EXPECT_NEAR(b.max.x, 5.0, 0.6);
888
+ EXPECT_NEAR(b.max.y, 10.0, 0.6);
889
+ EXPECT_NEAR(b.max.z, 20.0, 0.6);
890
+ }
891
+
892
+ TEST(LevelSet, IsovalueSelectsTheSurface) {
893
+ Evaluated small = evalSrc(sphereFieldSrc(40, 30) +
894
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=10);");
895
+ Evaluated big = evalSrc(sphereFieldSrc(40, 30) +
896
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
897
+ EXPECT_LT(soleBody(small).Volume(), soleBody(big).Volume());
898
+ }
899
+
900
+ TEST(LevelSet, BoundsPlaceTheResult) {
901
+ Evaluated e = evalSrc(sphereFieldSrc(40, 30) +
902
+ "levelset(f, bounds=[[100,-30,-30],[160,30,30]], isovalue=20);");
903
+ ASSERT_EQ(e.bodies.size(), 1u);
904
+ const manifold::Box b = soleBody(e).BoundingBox();
905
+ EXPECT_NEAR((b.min.x + b.max.x) / 2, 130.0, 1.0);
906
+ }
907
+
908
+ TEST(LevelSet, AnIsovalueNothingReachesIsEmpty) {
909
+ // A distance field is never negative, so nothing is inside. Not an
910
+ // error: asking is a fair question.
911
+ Evaluated e = evalSrc(sphereFieldSrc(20, 30) +
912
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=-1);");
913
+ EXPECT_TRUE(e.bodies.empty());
914
+ }
915
+
916
+ TEST(LevelSet, AnIsovalueEverythingSatisfiesFillsTheBounds) {
917
+ // The mirror case, and it is NOT empty: if every sample is inside, the
918
+ // solid is the whole box, clipped at the bounds. Worth pinning, because
919
+ // "nothing crossed the surface" and "everything is inside" look alike
920
+ // from the outside and mean opposite things.
921
+ Evaluated e = evalSrc(sphereFieldSrc(20, 30) +
922
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=1000);");
923
+ ASSERT_EQ(e.bodies.size(), 1u);
924
+ const manifold::Box b = soleBody(e).BoundingBox();
925
+ EXPECT_NEAR(b.max.x - b.min.x, 60.0, 1.0);
926
+ EXPECT_NEAR(soleBody(e).Volume(), 60.0 * 60.0 * 60.0, 0.02 * 60.0 * 60.0 * 60.0);
927
+ }
928
+
929
+ TEST(LevelSet, MalformedFieldsWarn) {
930
+ for (const char* code : {
931
+ "levelset([[[1,2],[3,4]],[[5,6],[7]]], bounds=[[0,0,0],[1,1,1]]);", // ragged
932
+ "levelset([[[1,2],[3,\"x\"]],[[5,6],[7,8]]], bounds=[[0,0,0],[1,1,1]]);", // not numeric
933
+ "levelset(5, bounds=[[0,0,0],[1,1,1]]);", // not a field
934
+ "levelset([[[1,2],[3,4]],[[5,6],[7,8]]], bounds=[[0,0,0],[0,1,1]]);", // empty extent
935
+ }) {
936
+ EXPECT_FALSE(levelsetWarnings(code).empty()) << "silent on: " << code;
937
+ }
938
+ }
939
+
940
+ // -- levelset, function form ----------------------------------------------
941
+ //
942
+ // Same builtin, field given as function(x,y,z) instead of a grid. Slower --
943
+ // a closure call per sample, and canParallel must be false because the
944
+ // evaluator would otherwise be called back from Manifold's worker threads --
945
+ // but MORE accurate, because Manifold picks its own sample points and can
946
+ // snap toward the true surface instead of interpolating a fixed lattice.
947
+ //
948
+ // Measured at matched resolution, sphere against analytic:
949
+ // 50^3 grid 0.23s (-0.244%) function 0.32s (-0.103%)
950
+ // 100^3 grid 1.06s (-0.059%) function 1.91s (-0.025%)
951
+
952
+ TEST(LevelSetFn, AFunctionFieldGivesTheSameSphere) {
953
+ Evaluated e = evalSrc(
954
+ "levelset(function(x,y,z) sqrt(x*x+y*y+z*z), "
955
+ "bounds=[[-30,-30,-30],[30,30,30]], isovalue=20, edge=1.5);");
956
+ ASSERT_EQ(e.bodies.size(), 1u);
957
+ const double analytic = 4.0 / 3.0 * 3.14159265358979 * 20 * 20 * 20;
958
+ EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.01 * analytic);
959
+ EXPECT_EQ(soleBody(e).Genus(), 0);
960
+ }
961
+
962
+ TEST(LevelSetFn, AgreesWithTheGridFormOnTheSameField) {
963
+ // The two intakes must describe the same solid; they differ only in how
964
+ // finely they can resolve it.
965
+ Evaluated grid = evalSrc(sphereFieldSrc(50, 30) +
966
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
967
+ Evaluated fn = evalSrc(
968
+ "levelset(function(x,y,z) sqrt(x*x+y*y+z*z), "
969
+ "bounds=[[-30,-30,-30],[30,30,30]], isovalue=20, edge=" +
970
+ std::to_string(60.0 / 49.0) + ");");
971
+ ASSERT_EQ(grid.bodies.size(), 1u);
972
+ ASSERT_EQ(fn.bodies.size(), 1u);
973
+ EXPECT_EQ(soleBody(fn).Genus(), soleBody(grid).Genus());
974
+ EXPECT_NEAR(soleBody(fn).Volume(), soleBody(grid).Volume(),
975
+ 0.01 * soleBody(grid).Volume());
976
+ }
977
+
978
+ TEST(LevelSetFn, TopologyIsFoundFromAFunctionToo) {
979
+ Evaluated e = evalSrc(
980
+ "levelset(function(x,y,z) let(q = sqrt(x*x+y*y) - 15) sqrt(q*q + z*z), "
981
+ "bounds=[[-30,-30,-30],[30,30,30]], isovalue=6, edge=1.2);");
982
+ ASSERT_EQ(e.bodies.size(), 1u);
983
+ EXPECT_EQ(soleBody(e).Genus(), 1);
984
+ }
985
+
986
+ TEST(LevelSetFn, AFunctionFieldRequiresEdge) {
987
+ // There is no grid to infer spacing from, and the cost is CUBIC in it --
988
+ // guessing would silently produce either a useless mesh or a ten-minute
989
+ // one. Making the caller say is the safer failure.
990
+ const std::vector<std::string> w = levelsetWarnings(
991
+ "levelset(function(x,y,z) sqrt(x*x+y*y+z*z), bounds=[[-1,-1,-1],[1,1,1]]);");
992
+ ASSERT_EQ(w.size(), 1u);
993
+ EXPECT_NE(w[0].find("edge="), std::string::npos) << w[0];
994
+ }
995
+
996
+ TEST(LevelSetFn, AFunctionOfTheWrongArityWarns) {
997
+ const std::vector<std::string> w = levelsetWarnings(
998
+ "levelset(function(p) 1, bounds=[[-1,-1,-1],[1,1,1]], edge=0.5);");
999
+ ASSERT_EQ(w.size(), 1u);
1000
+ EXPECT_NE(w[0].find("three parameters"), std::string::npos) << w[0];
1001
+ }
1002
+
1003
+ TEST(LevelSetFn, AFieldFunctionCanCaptureOuterVariables) {
1004
+ Evaluated e = evalSrc(
1005
+ "r = 20;\nlevelset(function(x,y,z) sqrt(x*x+y*y+z*z), "
1006
+ "bounds=[[-30,-30,-30],[30,30,30]], isovalue=r, edge=1.5);");
1007
+ ASSERT_EQ(e.bodies.size(), 1u);
1008
+ EXPECT_NEAR(soleBody(e).Volume(), 4.0 / 3.0 * 3.14159265358979 * 8000, 400.0);
1009
+ }
1010
+
1011
+ TEST(LevelSet, AnExplicitlyUndefIsovalueFallsBackToTheDefault) {
1012
+ // Same rule as linear_solve's b, and for the same reason: a wrapper
1013
+ // with a fixed signature forwards every parameter, undef included.
1014
+ Evaluated withUndef = evalSrc(sphereFieldSrc(30, 30) +
1015
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=undef);");
1016
+ Evaluated withZero = evalSrc(sphereFieldSrc(30, 30) +
1017
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=0);");
1018
+ EXPECT_EQ(withUndef.bodies.size(), withZero.bodies.size());
1019
+ EXPECT_TRUE(levelsetWarnings(sphereFieldSrc(30, 30) +
1020
+ "levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=undef);")
1021
+ .empty());
1022
+ }
@@ -35,7 +35,7 @@ std::vector<std::string> echoesFrom(const std::string& src) {
35
35
  // in featureLevels() -- shows up as a 0.
36
36
  const char* kFeatures[] = {"separate-children", "minkowski-diff", "sphere-styles", "export-name",
37
37
  "simplify-op", "expr-import", "object-function", "roof-op",
38
- "render-expr", "polyhedron-vnf", "linear-solve"};
38
+ "render-expr", "polyhedron-vnf", "linear-solve", "levelset"};
39
39
 
40
40
  } // namespace
41
41
 
@@ -883,3 +883,17 @@ TEST(LinearSolve, WorksUnderBothEngines) {
883
883
  }
884
884
  }
885
885
 
886
+
887
+ TEST(LinearSolve, AnExplicitlyUndefRightHandSideCountsAsAbsent) {
888
+ // BOSL2's builtins.scad pattern is a wrapper with a fixed signature:
889
+ // function _linear_solve(A, b) = linear_solve(A, b);
890
+ // so a determinant()-style caller that passes no right-hand side still
891
+ // forwards b as undef. Treating that as a BAD argument would warn on
892
+ // every such call. Passing undef is idiomatically the same as not
893
+ // passing in OpenSCAD -- BOSL2 threads optional arguments through
894
+ // wrappers that way throughout.
895
+ Evaluator ev;
896
+ EXPECT_NEAR(asNum(evalSrc("linear_solve([[2,1],[1,3]], undef).det", ev)), 5.0, 1e-12);
897
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[2,1],[1,3]], undef).x", ev)));
898
+ EXPECT_TRUE(builtinWarnings("linear_solve([[2,1],[1,3]], undef)").empty());
899
+ }