openscad-cpp-evaluator 0.50.0__tar.gz → 0.51.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 (208) hide show
  1. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/CLAUDE.md +29 -2
  2. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/function_builtins.cpp +13 -3
  5. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/topology.cpp +351 -48
  6. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_booleans.cpp +139 -3
  7. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_function_builtins.cpp +14 -0
  8. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/.github/workflows/ci.yml +0 -0
  9. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/.github/workflows/wheels.yml +0 -0
  10. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/.gitignore +0 -0
  11. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/.gitmodules +0 -0
  12. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/CMakeLists.txt +0 -0
  13. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/LICENSE +0 -0
  14. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/README.md +0 -0
  15. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/bindings/CMakeLists.txt +0 -0
  16. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/bindings/ast_to_py.cpp +0 -0
  17. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/bindings/module.cpp +0 -0
  18. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/cmake/embed_font.cmake +0 -0
  19. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/examples/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/examples/eval_perf_benchmark.cpp +0 -0
  21. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/examples/manifold_cache_reuse.cpp +0 -0
  22. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/examples/minimal_debugger.cpp +0 -0
  23. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  24. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/.gitignore +0 -0
  25. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  26. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  27. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/README.md +0 -0
  28. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  29. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  30. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  31. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  32. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  33. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  34. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  35. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  36. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  37. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  38. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  39. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  40. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  41. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  42. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  43. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  44. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  45. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  46. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  47. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  48. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  49. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  50. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  51. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  52. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  53. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  54. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  55. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  56. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  57. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  58. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  59. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  60. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  61. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  62. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  63. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  64. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  65. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  66. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  67. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  68. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  69. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  70. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  71. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  72. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  73. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  74. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  75. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  76. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  77. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  78. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  79. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  80. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  81. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  82. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  83. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  84. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  85. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  86. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  87. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  88. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  89. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  90. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  91. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  92. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  93. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  94. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  95. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  96. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  97. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  98. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  99. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  100. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  101. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  102. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  103. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  104. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  105. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  106. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  107. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  108. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  109. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  110. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  111. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  112. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  113. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  114. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  115. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  116. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  117. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  118. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  119. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  120. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  121. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  122. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  123. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  124. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/resources/fonts/LICENSE +0 -0
  125. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  126. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/CMakeLists.txt +0 -0
  127. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/booleans.cpp +0 -0
  128. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/builtins.hpp +0 -0
  129. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/call_args.cpp +0 -0
  130. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/color.cpp +0 -0
  131. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/control.cpp +0 -0
  132. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/dxf_dim.cpp +0 -0
  133. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/extrude.cpp +0 -0
  134. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/import.cpp +0 -0
  135. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/modifiers.cpp +0 -0
  136. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/primitives_2d.cpp +0 -0
  137. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/primitives_3d.cpp +0 -0
  138. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/registry.cpp +0 -0
  139. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/roof.cpp +0 -0
  140. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/segments.cpp +0 -0
  141. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/surface.cpp +0 -0
  142. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/text.cpp +0 -0
  143. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/builtins/transforms.cpp +0 -0
  144. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/bytecode_compiler.cpp +0 -0
  145. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/bytecode_vm.cpp +0 -0
  146. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/colored_body.cpp +0 -0
  147. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/csg_generate.cpp +0 -0
  148. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/csg_resolve.cpp +0 -0
  149. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/css_colors.cpp +0 -0
  150. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/debug/debug_repl.cpp +0 -0
  151. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/debug_profile.cpp +0 -0
  152. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/eval_context.cpp +0 -0
  153. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/eval_error.cpp +0 -0
  154. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/eval_use.cpp +0 -0
  155. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/export.cpp +0 -0
  156. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/expr_eval.cpp +0 -0
  157. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/import/dxf_import.cpp +0 -0
  158. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/import/mesh_import.cpp +0 -0
  159. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/import/surface_load.cpp +0 -0
  160. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/import/svg_import.cpp +0 -0
  161. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/manifold_cache.cpp +0 -0
  162. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/measure_geometry.cpp +0 -0
  163. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/mesh_check.cpp +0 -0
  164. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/stmt_eval.cpp +0 -0
  165. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/text/font_match.cpp +0 -0
  166. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/text/freetype_font_provider.cpp +0 -0
  167. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/text/text_metrics.cpp +0 -0
  168. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/user_calls.cpp +0 -0
  169. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/value.cpp +0 -0
  170. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/src/zip_stored.cpp +0 -0
  171. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/CMakeLists.txt +0 -0
  172. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_bytecode_compiler.cpp +0 -0
  173. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_children_separate.cpp +0 -0
  174. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_cli.cpp +0 -0
  175. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_control_flow.cpp +0 -0
  176. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_csg_tree.cpp +0 -0
  177. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_debug_hooks.cpp +0 -0
  178. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_dxf_svg_import.cpp +0 -0
  179. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_eval_context.cpp +0 -0
  180. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_export.cpp +0 -0
  181. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_expr_eval.cpp +0 -0
  182. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_extrude_roof.cpp +0 -0
  183. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_feature_detection.cpp +0 -0
  184. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_helpers.hpp +0 -0
  185. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_import_export.cpp +0 -0
  186. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_manifold_cache.cpp +0 -0
  187. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_mesh_check.cpp +0 -0
  188. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_multi_color_merge.cpp +0 -0
  189. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_primitives.cpp +0 -0
  190. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_profiling.cpp +0 -0
  191. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_python_bindings.py +0 -0
  192. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_render_expr.cpp +0 -0
  193. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_scope_trail.cpp +0 -0
  194. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_scoping.cpp +0 -0
  195. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_surface.cpp +0 -0
  196. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_tail_calls.cpp +0 -0
  197. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_text.cpp +0 -0
  198. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_transforms.cpp +0 -0
  199. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_unexpected_args.cpp +0 -0
  200. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_use.cpp +0 -0
  201. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.50.0 → openscad_cpp_evaluator-0.51.0}/uv.lock +0 -0
@@ -461,8 +461,17 @@ grep for `ponytail:`.
461
461
  and both read as `undef` in OpenSCAD, so a guard written against them works in both.
462
462
 
463
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.
464
+ a solid (or a section) from an implicit surface. **2D or 3D is decided by `bounds`**, not guessed
465
+ from the field: a 2-vector corner means a section, a 3-vector means a solid. `field` is an array
466
+ (`field[i][j]` or `field[i][j][k]`, i,j,k → x,y,z) or a `function(x,y)`/`function(x,y,z)`.
467
+ The array is **not parsed until `bounds` has been read** — parsing it as 3D up front rejected
468
+ every 2D grid before the 2D path could run.
469
+ **`isovalue` is a band**: a scalar `v` is `[-INF, v]` ("at or below", the distance-field
470
+ reading), a range means *between*, and `[lo, INF]` is BOSL2's "at or above" idiom — so code
471
+ forwarding BOSL2's own isovalue gets BOSL2's semantics with no translation and no `invert`.
472
+ One `LevelSet` pass either way; a bounded range *could* be
473
+ `difference(levelset(hi), levelset(lo))` but that meshes twice and then booleans.
474
+ 3D wraps `Manifold::LevelSet` with a C++ sampling lambda; no new Manifold API.
466
475
  **The field is a grid OR a `function(x,y,z)`, and the two trade off against each other** —
467
476
  neither is simply better:
468
477
 
@@ -492,6 +501,24 @@ grep for `ponytail:`.
492
501
  same reason.
493
502
  Calling the field function from the generate pass has no live `EvalContext`, so a root is built
494
503
  from the closure's own scope — `$`-variables sit at their defaults inside it.
504
+ **An explicitly-undef optional argument counts as ABSENT**, not as a bad one — the same rule in
505
+ `linear_solve` (`b`) and `levelset` (`isovalue`). BOSL2 reaches a builtin it also defines by
506
+ wrapping it in `builtins.scad`, which `use <>`s nothing, so the names there bind to the builtins:
507
+ `function _linear_solve(A, b) = linear_solve(A, b);`. That wrapper has a fixed signature and so
508
+ forwards `b` even when the caller passed none, so treating undef as a *bad* argument warned on
509
+ every `determinant()`-style call. Passing undef is idiomatically the same as not passing in
510
+ OpenSCAD, which is how BOSL2 threads optional arguments through wrappers throughout.
511
+ Verified against OpenSCAD 2026.02.01: the wrapper pattern resolves identically there.
512
+ **2D is marching squares here** — `CrossSection` has no contour extraction, only construction
513
+ from polygons. Three details carry the correctness: saddles (cases 5/10) resolved by the
514
+ cell-centre average, or the topology is a coin flip; vertices keyed on **edge identity** so
515
+ loops close exactly rather than nearly; and the field padded with an outside ring so no contour
516
+ runs off the box.
517
+ **The padding then has to be clipped back off.** Measured: a half-plane came out **0.9 × spacing
518
+ too large along every side it touched** — an O(h) error where a closed contour is O(h²) — because
519
+ the contour landed out in the pad. `clipToBounds` intersects with the `bounds` rectangle and the
520
+ answer becomes exact and resolution-independent (`LevelSet2d.AContourRunningOffTheBoxIsClippedExactly`).
521
+ Do not remove it, and do not "fix" it by moving the pad closer instead.
495
522
  Sign convention: Manifold takes positive as inside; a distance field is the other way round, so
496
523
  the default flips it and `invert=true` flips back.
497
524
  Two behaviours that look alike and are opposite: an isovalue **nothing reaches** gives empty
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.50.0
3
+ Version: 0.51.0
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.50.0"
7
+ version = "0.51.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1345,9 +1345,19 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
1345
1345
  // The OpenSCAD release we track. version_num() is that same
1346
1346
  // year/month/day folded as y * 10000 + m * 100 + d, exactly like the
1347
1347
  // reference's own builtin_version_num (builtin_functions.cc).
1348
- case BuiltinFnId::LinearSolve:
1349
- return builtinLinearSolve(ev, getArg(args, 0, "A", Value{}), getArg(args, 1, "b", Value{}),
1350
- 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
+ }
1351
1361
  case BuiltinFnId::SupportedFeature: {
1352
1362
  const Value name = getArg(args, 0, "feature", Value{});
1353
1363
  const std::string* s = std::get_if<std::string>(&name);
@@ -512,6 +512,63 @@ std::optional<ScalarField> readField(const Value& val) {
512
512
  return f;
513
513
  }
514
514
 
515
+ // field[i][j] -> rows of equal length. The 2D twin of readField.
516
+ std::optional<std::vector<std::vector<double>>> readPlane(const Value& val) {
517
+ const ListPtr* xs = std::get_if<ListPtr>(&val);
518
+ if (!xs || !*xs || (*xs)->items.empty()) return std::nullopt;
519
+ std::vector<std::vector<double>> out;
520
+ out.reserve((*xs)->items.size());
521
+ size_t ny = 0;
522
+ for (size_t i = 0; i < (*xs)->items.size(); ++i) {
523
+ const ListPtr* ys = std::get_if<ListPtr>(&(*xs)->items[i]);
524
+ if (!ys || !*ys || (*ys)->items.empty()) return std::nullopt;
525
+ if (i == 0) ny = (*ys)->items.size();
526
+ else if ((*ys)->items.size() != ny) return std::nullopt;
527
+ std::vector<double> row;
528
+ row.reserve(ny);
529
+ for (const Value& item : (*ys)->items) {
530
+ const double* d = std::get_if<double>(&item);
531
+ if (!d || !std::isfinite(*d)) return std::nullopt;
532
+ row.push_back(*d);
533
+ }
534
+ out.push_back(std::move(row));
535
+ }
536
+ return out;
537
+ }
538
+
539
+ // Like a numeric list, but INF and -INF are legal -- BOSL2 writes its
540
+ // open-ended ranges as [isovalue, INF].
541
+ std::optional<std::vector<double>> numbersOrInf(const Value& v) {
542
+ const ListPtr* l = std::get_if<ListPtr>(&v);
543
+ if (!l || !*l) return std::nullopt;
544
+ std::vector<double> out;
545
+ out.reserve((*l)->items.size());
546
+ for (const Value& item : (*l)->items) {
547
+ const double* d = std::get_if<double>(&item);
548
+ if (!d || std::isnan(*d)) return std::nullopt;
549
+ out.push_back(*d);
550
+ }
551
+ return out;
552
+ }
553
+
554
+ // Positive inside the band [lo, hi], zero on either surface. Manifold takes
555
+ // POSITIVE as inside and imposes no continuity or true-distance requirement,
556
+ // so the min() kink at the middle of a bounded band is fine.
557
+ //
558
+ // One pass, not two: a bounded range could be had as
559
+ // difference(levelset(hi), levelset(lo)), and that works, but it meshes
560
+ // twice and then does a boolean.
561
+ double bandDistance(double v, double lo, double hi, bool invert) {
562
+ const bool loFinite = std::isfinite(lo);
563
+ const bool hiFinite = std::isfinite(hi);
564
+ double d;
565
+ if (loFinite && hiFinite) d = std::min(v - lo, hi - v);
566
+ else if (hiFinite) d = hi - v; // [-INF, hi] -- a scalar isovalue
567
+ else if (loFinite) d = v - lo; // [lo, INF] -- BOSL2's idiom
568
+ else d = 1.0; // unbounded both ways: everything is inside
569
+ return invert ? -d : d;
570
+ }
571
+
515
572
  std::optional<std::array<double, 3>> readVec3(const Value& v) {
516
573
  const ListPtr* l = std::get_if<ListPtr>(&v);
517
574
  if (!l || !*l || (*l)->items.size() != 3) return std::nullopt;
@@ -526,6 +583,130 @@ std::optional<std::array<double, 3>> readVec3(const Value& v) {
526
583
 
527
584
  } // namespace
528
585
 
586
+ // -- 2D: marching squares --------------------------------------------------
587
+ //
588
+ // CrossSection has no contour extraction -- it only builds from explicit
589
+ // polygons -- so the contours are produced here and handed to
590
+ // CrossSection(Polygons, FillRule).
591
+ //
592
+ // Three details are where the bugs live in this algorithm, so all three are
593
+ // handled explicitly rather than hoped for:
594
+ //
595
+ // * SADDLES. Cases 5 and 10 have two opposite corners inside and two out,
596
+ // and admit two different connections. Picking wrong changes the
597
+ // TOPOLOGY -- two touching blobs versus one pinched shape. Resolved with
598
+ // the cell-centre average (the asymptotic decider).
599
+ // * SHARED VERTICES. Segments are keyed on EDGE IDENTITY, never on float
600
+ // coordinates. Two cells sharing an edge then produce the same vertex id
601
+ // by construction, so loops close exactly instead of nearly.
602
+ // * THE BOUNDARY. A contour running off the box would be an open path. The
603
+ // field is padded with a ring of "outside" first, so every contour closes.
604
+ // That is BOSL2's closed=true, and the only behaviour offered here.
605
+
606
+ struct Contours2d {
607
+ std::vector<double> v; // (ny+2) x (nx+2), padded
608
+ size_t nx = 0, ny = 0; // padded dimensions
609
+ double ox = 0, oy = 0, dx = 1, dy = 1; // origin/spacing of the PADDED grid
610
+ double at(size_t i, size_t j) const { return v[j * nx + i]; }
611
+ };
612
+
613
+ // The padded ring sits a full spacing outside the box, so a contour that
614
+ // runs off the edge lands out in the padding -- measured, that inflated a
615
+ // half-plane by 0.9 * spacing along every side it touched, an O(h) error
616
+ // where a closed contour is O(h^2). Clipping to the box afterwards removes
617
+ // exactly that overhang, and is what the caller means by `bounds` anyway.
618
+ manifold::CrossSection clipToBounds(const manifold::CrossSection& cs, const std::vector<double>& lo,
619
+ const std::vector<double>& hi) {
620
+ manifold::Rect box(manifold::vec2(lo[0], lo[1]), manifold::vec2(hi[0], hi[1]));
621
+ return cs ^ manifold::CrossSection(box);
622
+ }
623
+
624
+ manifold::Polygons marchingSquares(const Contours2d& g) {
625
+ const size_t nx = g.nx, ny = g.ny;
626
+ // A vertex can sit on a horizontal edge (between i,j and i+1,j) or a
627
+ // vertical one (between i,j and i,j+1). Ids are derived from the edge,
628
+ // so the two cells sharing it agree exactly.
629
+ const auto hId = [&](size_t i, size_t j) { return j * (nx - 1) + i; };
630
+ const size_t hCount = (nx - 1) * ny;
631
+ const auto vId = [&](size_t i, size_t j) { return hCount + j * nx + i; };
632
+
633
+ std::unordered_map<size_t, manifold::vec2> pts;
634
+ const auto lerpEdge = [&](size_t i0, size_t j0, size_t i1, size_t j1, size_t id) {
635
+ if (pts.count(id)) return;
636
+ const double a = g.at(i0, j0), b = g.at(i1, j1);
637
+ const double t = (a == b) ? 0.5 : a / (a - b);
638
+ pts[id] = manifold::vec2(g.ox + (static_cast<double>(i0) + t * (static_cast<double>(i1) - static_cast<double>(i0))) * g.dx,
639
+ g.oy + (static_cast<double>(j0) + t * (static_cast<double>(j1) - static_cast<double>(j0))) * g.dy);
640
+ };
641
+
642
+ std::vector<std::pair<size_t, size_t>> segs;
643
+ for (size_t j = 0; j + 1 < ny; ++j) {
644
+ for (size_t i = 0; i + 1 < nx; ++i) {
645
+ const double d0 = g.at(i, j), d1 = g.at(i + 1, j), d2 = g.at(i + 1, j + 1), d3 = g.at(i, j + 1);
646
+ const int mask = (d0 > 0 ? 1 : 0) | (d1 > 0 ? 2 : 0) | (d2 > 0 ? 4 : 0) | (d3 > 0 ? 8 : 0);
647
+ if (mask == 0 || mask == 15) continue;
648
+
649
+ const size_t eB = hId(i, j), eT = hId(i, j + 1), eL = vId(i, j), eR = vId(i + 1, j);
650
+ const auto mkB = [&] { lerpEdge(i, j, i + 1, j, eB); };
651
+ const auto mkT = [&] { lerpEdge(i, j + 1, i + 1, j + 1, eT); };
652
+ const auto mkL = [&] { lerpEdge(i, j, i, j + 1, eL); };
653
+ const auto mkR = [&] { lerpEdge(i + 1, j, i + 1, j + 1, eR); };
654
+ const auto add = [&](size_t a, size_t b) { segs.emplace_back(a, b); };
655
+
656
+ switch (mask) {
657
+ case 1: case 14: mkL(); mkB(); add(eL, eB); break;
658
+ case 2: case 13: mkB(); mkR(); add(eB, eR); break;
659
+ case 3: case 12: mkL(); mkR(); add(eL, eR); break;
660
+ case 4: case 11: mkR(); mkT(); add(eR, eT); break;
661
+ case 6: case 9: mkB(); mkT(); add(eB, eT); break;
662
+ case 8: case 7: mkT(); mkL(); add(eT, eL); break;
663
+ case 5: case 10: {
664
+ // Saddle: the centre decides which way the two contours
665
+ // pass. Without this the topology is a coin flip.
666
+ mkL(); mkB(); mkR(); mkT();
667
+ const double centre = 0.25 * (d0 + d1 + d2 + d3);
668
+ const bool joinLB = (mask == 5) == (centre > 0);
669
+ if (joinLB) { add(eL, eB); add(eR, eT); }
670
+ else { add(eL, eT); add(eB, eR); }
671
+ break;
672
+ }
673
+ default: break;
674
+ }
675
+ }
676
+ }
677
+ if (segs.empty()) return {};
678
+
679
+ // Chain segments into closed loops. Every vertex sits on exactly two
680
+ // segments in a well-formed field, so this is a plain walk.
681
+ std::unordered_map<size_t, std::vector<size_t>> adj;
682
+ for (const auto& [a, b] : segs) {
683
+ adj[a].push_back(b);
684
+ adj[b].push_back(a);
685
+ }
686
+ std::unordered_set<size_t> used;
687
+ manifold::Polygons out;
688
+ for (const auto& [start, _] : adj) {
689
+ if (used.count(start)) continue;
690
+ manifold::SimplePolygon loop;
691
+ size_t cur = start, prev = SIZE_MAX;
692
+ while (true) {
693
+ if (used.count(cur)) break;
694
+ used.insert(cur);
695
+ loop.push_back(pts[cur]);
696
+ size_t next = SIZE_MAX;
697
+ for (size_t cand : adj[cur]) {
698
+ if (cand != prev && !used.count(cand)) { next = cand; break; }
699
+ }
700
+ if (next == SIZE_MAX) break;
701
+ prev = cur;
702
+ cur = next;
703
+ }
704
+ if (loop.size() >= 3) out.push_back(std::move(loop));
705
+ }
706
+ return out;
707
+ }
708
+
709
+
529
710
  CSGParams resolveLevelSet(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
530
711
  auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
531
712
  CSGParams params;
@@ -553,66 +734,85 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
553
734
  // back from threads it knows nothing about.
554
735
  const Value& fieldArg = params.at("field");
555
736
  const ClosurePtr* fieldFn = std::get_if<ClosurePtr>(&fieldArg);
737
+ // The array is NOT parsed yet: whether it should be field[i][j] or
738
+ // field[i][j][k] depends on `bounds`, which is read below. Parsing it as
739
+ // 3D here rejected every 2D grid before the 2D path ever ran.
556
740
  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) ...",
741
+ if (fieldFn && (!*fieldFn || (*fieldFn)->node == nullptr || (*fieldFn)->node->parameters.size() < 2)) {
742
+ ev.warn("levelset(): the field function needs function(x,y) for 2D or function(x,y,z) for 3D",
570
743
  &node.position());
571
744
  return {};
572
745
  }
573
746
 
747
+ // 2D or 3D is decided by `bounds`, not guessed from the field: a 2-vector
748
+ // corner means a section, a 3-vector means a solid. Explicit, and it
749
+ // matches how the caller already has to think about the box.
574
750
  const ListPtr* bb = std::get_if<ListPtr>(&params.at("bounds"));
575
- std::optional<std::array<double, 3>> lo, hi;
751
+ std::optional<std::vector<double>> bLo, bHi;
576
752
  if (bb && *bb && (*bb)->items.size() == 2) {
577
- lo = readVec3((*bb)->items[0]);
578
- hi = readVec3((*bb)->items[1]);
753
+ bLo = numbersOrInf((*bb)->items[0]);
754
+ bHi = numbersOrInf((*bb)->items[1]);
579
755
  }
580
- if (!lo || !hi) {
581
- ev.warn("levelset(): bounds must be [[x0,y0,z0],[x1,y1,z1]]", &node.position());
756
+ if (!bLo || !bHi || bLo->size() != bHi->size() || (bLo->size() != 2 && bLo->size() != 3)) {
757
+ ev.warn("levelset(): bounds must be [[x0,y0],[x1,y1]] or [[x0,y0,z0],[x1,y1,z1]]",
758
+ &node.position());
582
759
  return {};
583
760
  }
584
- for (int a = 0; a < 3; ++a) {
585
- if (!((*hi)[a] > (*lo)[a])) {
761
+ const bool is2d = bLo->size() == 2;
762
+ std::optional<std::array<double, 3>> lo, hi;
763
+ if (!is2d) {
764
+ lo = std::array<double, 3>{(*bLo)[0], (*bLo)[1], (*bLo)[2]};
765
+ hi = std::array<double, 3>{(*bHi)[0], (*bHi)[1], (*bHi)[2]};
766
+ }
767
+ for (size_t a = 0; a < bLo->size(); ++a) {
768
+ if (!((*bHi)[a] > (*bLo)[a])) {
586
769
  ev.warn("levelset(): bounds must be increasing along every axis", &node.position());
587
770
  return {};
588
771
  }
589
772
  }
590
773
 
591
- const double* isoArg = std::get_if<double>(&params.at("isovalue"));
592
- if (!isoArg) {
593
- ev.warn("levelset(): isovalue must be a number", &node.position());
594
- return {};
774
+ // isovalue is a single level OR a bounded range [lo, hi].
775
+ //
776
+ // Unified as a band: a scalar v is [-INF, v], so "at or below" -- the
777
+ // distance-field reading, where smaller means further inside. A range
778
+ // means BETWEEN, which is what BOSL2's isosurface/contour pass around,
779
+ // and [lo, INF] is its "at or above" idiom. A caller handing us BOSL2's
780
+ // own isovalue argument therefore gets BOSL2's own semantics with no
781
+ // translation and no invert.
782
+ //
783
+ // Same rule as linear_solve's b: an explicitly-undef argument is ABSENT,
784
+ // so a fixed-signature wrapper forwarding every parameter still works.
785
+ const Value& isoVal = params.at("isovalue");
786
+ double isoLo = -std::numeric_limits<double>::infinity();
787
+ double isoHi = 0.0;
788
+ if (!std::holds_alternative<std::monostate>(isoVal)) {
789
+ if (const double* isoArg = std::get_if<double>(&isoVal)) {
790
+ isoHi = *isoArg;
791
+ } else if (const std::optional<std::vector<double>> pair = numbersOrInf(isoVal);
792
+ pair && pair->size() == 2) {
793
+ isoLo = (*pair)[0];
794
+ isoHi = (*pair)[1];
795
+ if (!(isoHi > isoLo)) {
796
+ ev.warn("levelset(): isovalue range must be increasing", &node.position());
797
+ return {};
798
+ }
799
+ } else {
800
+ ev.warn("levelset(): isovalue must be a number or a [low, high] range", &node.position());
801
+ return {};
802
+ }
595
803
  }
596
- const double isovalue = *isoArg;
597
804
  const bool invert = truthy(params.at("invert"));
598
805
 
599
806
  const std::array<double, 3> origin = *lo;
600
807
  std::array<double, 3> spacing{1.0, 1.0, 1.0};
601
808
  double edge = 0.0;
602
- if (field) {
603
- // Grid spacing is corner-sample to corner-sample, so n-1 intervals.
604
- spacing = {((*hi)[0] - (*lo)[0]) / static_cast<double>(field->nx - 1),
605
- ((*hi)[1] - (*lo)[1]) / static_cast<double>(field->ny - 1),
606
- ((*hi)[2] - (*lo)[2]) / static_cast<double>(field->nz - 1)};
607
- // Finer than the grid buys nothing -- there is no information between
608
- // samples -- and coarser throws away what the script paid to compute.
609
- edge = std::min({spacing[0], spacing[1], spacing[2]});
610
- }
809
+ // Grid spacing is filled in below, once the array has been read at the
810
+ // right dimensionality. Finer than the grid buys nothing.
611
811
  if (const double* e = std::get_if<double>(&params.at("edge"))) {
612
812
  if (*e > 0.0) edge = *e;
613
813
  else ev.warn("levelset(): edge must be positive", &node.position());
614
814
  }
615
- if (edge <= 0.0) {
815
+ if (edge <= 0.0 && fieldFn) {
616
816
  // No grid to infer it from. Guessing would silently pick either a
617
817
  // useless mesh or a ten-minute one -- the cost is cubic in this
618
818
  // number, so it is the caller's call to make.
@@ -621,27 +821,133 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
621
821
  }
622
822
 
623
823
  // The function form: one closure call per sample. No live EvalContext at
624
- // generate time, so a root is built from the closure's own scope -- the
625
- // same approach the warp experiment used. $-variables are therefore at
626
- // their defaults inside a field function.
824
+ // generate time, so a root is built from the closure's own scope. Its
825
+ // $-variables are therefore at their defaults.
627
826
  std::optional<EvalContext> fnCtx;
628
- std::array<std::string, 3> fnParams;
827
+ std::vector<std::string> fnParams2;
629
828
  if (fieldFn) {
630
829
  fnCtx = EvalContext::makeRoot((*fieldFn)->node->scope());
631
- for (int a = 0; a < 3; ++a) fnParams[static_cast<size_t>(a)] = (*fieldFn)->node->parameters[static_cast<size_t>(a)]->name->name;
830
+ for (const auto& prm : (*fieldFn)->node->parameters) fnParams2.push_back(prm->name->name);
831
+ }
832
+
833
+ // ---- 2D: sample onto a padded grid, contour it, hand to CrossSection.
834
+ if (is2d) {
835
+ size_t nx = 0, ny = 0;
836
+ double sx = 0, sy = 0;
837
+ if (field) {
838
+ ev.warn("levelset(): a 2D field must be field[i][j]; a 3D array was given",
839
+ &node.position());
840
+ return {};
841
+ }
842
+ if (!fieldFn) {
843
+ const std::optional<std::vector<std::vector<double>>> plane = readPlane(fieldArg);
844
+ if (!plane || plane->size() < 2 || (*plane)[0].size() < 2) {
845
+ ev.warn("levelset(): 2D field must be a rectangular field[i][j] of numbers",
846
+ &node.position());
847
+ return {};
848
+ }
849
+ nx = plane->size();
850
+ ny = (*plane)[0].size();
851
+ sx = ((*bHi)[0] - (*bLo)[0]) / static_cast<double>(nx - 1);
852
+ sy = ((*bHi)[1] - (*bLo)[1]) / static_cast<double>(ny - 1);
853
+ Contours2d g;
854
+ g.nx = nx + 2;
855
+ g.ny = ny + 2;
856
+ g.dx = sx;
857
+ g.dy = sy;
858
+ g.ox = (*bLo)[0] - sx;
859
+ g.oy = (*bLo)[1] - sy;
860
+ // Padded with a ring that is firmly OUTSIDE, so a contour meeting
861
+ // the box edge closes along it instead of running off. This is
862
+ // BOSL2's closed=true, and the only behaviour offered.
863
+ g.v.assign(g.nx * g.ny, -1.0);
864
+ for (size_t i = 0; i < nx; ++i)
865
+ for (size_t j = 0; j < ny; ++j)
866
+ g.v[(j + 1) * g.nx + (i + 1)] = bandDistance((*plane)[i][j], isoLo, isoHi, invert);
867
+ const manifold::Polygons polys = marchingSquares(g);
868
+ if (polys.empty()) return {};
869
+ ColoredBody b;
870
+ b.section = clipToBounds(manifold::CrossSection(polys, manifold::CrossSection::FillRule::EvenOdd),
871
+ *bLo, *bHi);
872
+ if (b.section->IsEmpty()) return {};
873
+ std::vector<ColoredBody> out;
874
+ out.push_back(std::move(b));
875
+ return out;
876
+ }
877
+ // function(x,y): edge= gives the spacing, as in 3D
878
+ nx = static_cast<size_t>(std::floor(((*bHi)[0] - (*bLo)[0]) / edge)) + 1;
879
+ ny = static_cast<size_t>(std::floor(((*bHi)[1] - (*bLo)[1]) / edge)) + 1;
880
+ if (nx < 2 || ny < 2) {
881
+ ev.warn("levelset(): edge is larger than the bounds", &node.position());
882
+ return {};
883
+ }
884
+ sx = ((*bHi)[0] - (*bLo)[0]) / static_cast<double>(nx - 1);
885
+ sy = ((*bHi)[1] - (*bLo)[1]) / static_cast<double>(ny - 1);
886
+ Contours2d g;
887
+ g.nx = nx + 2;
888
+ g.ny = ny + 2;
889
+ g.dx = sx;
890
+ g.dy = sy;
891
+ g.ox = (*bLo)[0] - sx;
892
+ g.oy = (*bLo)[1] - sy;
893
+ g.v.assign(g.nx * g.ny, -1.0);
894
+ for (size_t i = 0; i < nx; ++i) {
895
+ for (size_t j = 0; j < ny; ++j) {
896
+ BoundArgs bound;
897
+ bound.set(fnParams2[0], Value{(*bLo)[0] + static_cast<double>(i) * sx});
898
+ bound.set(fnParams2[1], Value{(*bLo)[1] + static_cast<double>(j) * sy});
899
+ const Value outv =
900
+ ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
901
+ const double* d = std::get_if<double>(&outv);
902
+ const double val = (d && std::isfinite(*d)) ? *d : std::numeric_limits<double>::max();
903
+ g.v[(j + 1) * g.nx + (i + 1)] = bandDistance(val, isoLo, isoHi, invert);
904
+ }
905
+ }
906
+ const manifold::Polygons polys = marchingSquares(g);
907
+ if (polys.empty()) return {};
908
+ ColoredBody b;
909
+ b.section = clipToBounds(manifold::CrossSection(polys, manifold::CrossSection::FillRule::EvenOdd),
910
+ *bLo, *bHi);
911
+ if (b.section->IsEmpty()) return {};
912
+ std::vector<ColoredBody> out;
913
+ out.push_back(std::move(b));
914
+ return out;
915
+ }
916
+
917
+
918
+ if (!fieldFn) {
919
+ field = readField(fieldArg);
920
+ if (!field) {
921
+ ev.warn("levelset(): field must be a function(x,y,z) or a rectangular field[i][j][k] of numbers",
922
+ &node.position());
923
+ return {};
924
+ }
925
+ if (field->nx < 2 || field->ny < 2 || field->nz < 2) {
926
+ ev.warn("levelset(): the field needs at least 2 samples along each axis", &node.position());
927
+ return {};
928
+ }
929
+ spacing = {((*bHi)[0] - (*bLo)[0]) / static_cast<double>(field->nx - 1),
930
+ ((*bHi)[1] - (*bLo)[1]) / static_cast<double>(field->ny - 1),
931
+ ((*bHi)[2] - (*bLo)[2]) / static_cast<double>(field->nz - 1)};
932
+ if (!std::get_if<double>(&params.at("edge"))) edge = std::min({spacing[0], spacing[1], spacing[2]});
933
+ }
934
+
935
+ if (fieldFn && fnParams2.size() < 3) {
936
+ ev.warn("levelset(): a 3D field function needs three parameters, as in function(x,y,z) ...",
937
+ &node.position());
938
+ return {};
632
939
  }
633
940
 
634
941
  const auto sampleFn = [&](manifold::vec3 p) -> double {
635
942
  BoundArgs bound;
636
943
  const double xyz[3] = {p.x, p.y, p.z};
637
- for (int a = 0; a < 3; ++a) bound.set(fnParams[static_cast<size_t>(a)], Value{xyz[a]});
944
+ for (int a = 0; a < 3; ++a) bound.set(fnParams2[static_cast<size_t>(a)], Value{xyz[a]});
638
945
  const Value out = ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
639
946
  const double* d = std::get_if<double>(&out);
640
947
  // A field that returns junk at one point must not abort the whole
641
948
  // build; treat it as far outside instead.
642
949
  const double v = (d && std::isfinite(*d)) ? *d : std::numeric_limits<double>::max();
643
- const double diff = v - isovalue;
644
- return invert ? diff : -diff;
950
+ return bandDistance(v, isoLo, isoHi, invert);
645
951
  };
646
952
 
647
953
  // Never dereferenced on the function path, but it must still be a valid
@@ -649,7 +955,7 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
649
955
  // where the lambda is never called.
650
956
  const ScalarField emptyField;
651
957
  const ScalarField& f = field ? *field : emptyField;
652
- const auto sampleGrid = [&f, origin, spacing, isovalue, invert](manifold::vec3 p) -> double {
958
+ const auto sampleGrid = [&f, origin, spacing, isoLo, isoHi, invert](manifold::vec3 p) -> double {
653
959
  double g[3];
654
960
  size_t i0[3];
655
961
  double t[3];
@@ -671,10 +977,7 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
671
977
  const double w = (di ? t[0] : 1.0 - t[0]) * (dj ? t[1] : 1.0 - t[1]) * (dk ? t[2] : 1.0 - t[2]);
672
978
  acc += w * f.at(i0[0] + di, i0[1] + dj, i0[2] + dk);
673
979
  }
674
- // Manifold takes POSITIVE as inside. A distance field is the other
675
- // way round, which is the common case, so the default flips it.
676
- const double d = acc - isovalue;
677
- return invert ? d : -d;
980
+ return bandDistance(acc, isoLo, isoHi, invert);
678
981
  };
679
982
 
680
983
  manifold::Box bounds(manifold::vec3(origin[0], origin[1], origin[2]),