openscad-cpp-evaluator 0.55.0__tar.gz → 0.55.2__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.55.0 → openscad_cpp_evaluator-0.55.2}/CLAUDE.md +11 -0
  2. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/bindings/ast_to_py.cpp +8 -0
  4. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/bindings/module.cpp +8 -0
  5. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/csg_node.hpp +15 -0
  6. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/evaluator.hpp +2 -1
  7. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/pyproject.toml +1 -1
  8. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/python/openscad_cpp_evaluator/__init__.py +20 -1
  9. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/csg_generate.cpp +36 -7
  10. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/export.cpp +13 -0
  11. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/manifold_cache.cpp +38 -1
  12. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_booleans.cpp +46 -8
  13. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_manifold_cache.cpp +59 -0
  14. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_python_bindings.py +37 -0
  15. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/.github/workflows/ci.yml +0 -0
  16. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/.github/workflows/wheels.yml +0 -0
  17. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/.gitignore +0 -0
  18. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/.gitmodules +0 -0
  19. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/LICENSE +0 -0
  21. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/README.md +0 -0
  22. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/bindings/CMakeLists.txt +0 -0
  23. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/cmake/embed_font.cmake +0 -0
  24. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/examples/CMakeLists.txt +0 -0
  25. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/examples/eval_perf_benchmark.cpp +0 -0
  26. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/examples/manifold_cache_reuse.cpp +0 -0
  27. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/examples/minimal_debugger.cpp +0 -0
  28. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  29. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/.gitignore +0 -0
  30. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  31. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  32. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/README.md +0 -0
  33. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  34. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  35. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  36. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  37. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  38. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  39. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  40. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  41. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  42. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  43. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  44. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  45. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  46. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  47. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  48. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  49. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/api.cpp +0 -0
  50. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  51. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  52. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  53. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  54. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  55. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  56. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  57. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  58. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  59. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  60. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  61. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  62. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  63. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  64. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  65. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  66. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/position.cpp +0 -0
  67. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  68. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  69. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  70. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  71. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  72. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  73. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  74. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  75. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  76. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  77. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  78. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  79. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  80. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  81. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  82. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  83. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  84. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  85. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  86. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  87. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  88. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  89. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  90. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  91. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  92. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  93. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  94. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  95. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  96. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  97. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  98. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  99. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  100. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  101. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  102. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  103. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  104. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  105. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  106. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  107. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  108. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  109. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/export.hpp +0 -0
  110. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  111. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  112. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  113. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  114. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  115. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  116. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  117. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  118. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  119. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  120. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  121. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  122. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  123. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  124. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/value.hpp +0 -0
  125. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  126. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/resources/fonts/LICENSE +0 -0
  127. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  128. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/CMakeLists.txt +0 -0
  129. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/booleans.cpp +0 -0
  130. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/builtins.hpp +0 -0
  131. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/call_args.cpp +0 -0
  132. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/color.cpp +0 -0
  133. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/control.cpp +0 -0
  134. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/dxf_dim.cpp +0 -0
  135. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/extrude.cpp +0 -0
  136. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/function_builtins.cpp +0 -0
  137. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/import.cpp +0 -0
  138. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/modifiers.cpp +0 -0
  139. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/primitives_2d.cpp +0 -0
  140. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/primitives_3d.cpp +0 -0
  141. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/registry.cpp +0 -0
  142. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/roof.cpp +0 -0
  143. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/segments.cpp +0 -0
  144. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/surface.cpp +0 -0
  145. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/text.cpp +0 -0
  146. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/topology.cpp +0 -0
  147. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/builtins/transforms.cpp +0 -0
  148. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/bytecode_compiler.cpp +0 -0
  149. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/bytecode_vm.cpp +0 -0
  150. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/colored_body.cpp +0 -0
  151. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/csg_resolve.cpp +0 -0
  152. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/css_colors.cpp +0 -0
  153. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/debug/debug_repl.cpp +0 -0
  154. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/debug_profile.cpp +0 -0
  155. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/eval_context.cpp +0 -0
  156. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/eval_error.cpp +0 -0
  157. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/eval_use.cpp +0 -0
  158. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/expr_eval.cpp +0 -0
  159. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/import/dxf_import.cpp +0 -0
  160. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/import/mesh_import.cpp +0 -0
  161. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/import/surface_load.cpp +0 -0
  162. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/import/svg_import.cpp +0 -0
  163. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/measure_geometry.cpp +0 -0
  164. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/mesh_check.cpp +0 -0
  165. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/stmt_eval.cpp +0 -0
  166. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/text/font_match.cpp +0 -0
  167. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/text/freetype_font_provider.cpp +0 -0
  168. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/text/text_metrics.cpp +0 -0
  169. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/user_calls.cpp +0 -0
  170. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/value.cpp +0 -0
  171. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/src/zip_stored.cpp +0 -0
  172. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/CMakeLists.txt +0 -0
  173. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_bytecode_compiler.cpp +0 -0
  174. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_children_separate.cpp +0 -0
  175. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_cli.cpp +0 -0
  176. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_control_flow.cpp +0 -0
  177. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_csg_tree.cpp +0 -0
  178. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_debug_hooks.cpp +0 -0
  179. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_dxf_svg_import.cpp +0 -0
  180. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_eval_context.cpp +0 -0
  181. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_export.cpp +0 -0
  182. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_expr_eval.cpp +0 -0
  183. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_extrude_roof.cpp +0 -0
  184. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_feature_detection.cpp +0 -0
  185. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_function_builtins.cpp +0 -0
  186. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_helpers.hpp +0 -0
  187. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_import_export.cpp +0 -0
  188. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_mesh_check.cpp +0 -0
  189. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_multi_color_merge.cpp +0 -0
  190. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_primitives.cpp +0 -0
  191. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_profiling.cpp +0 -0
  192. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_render_expr.cpp +0 -0
  193. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_scope_trail.cpp +0 -0
  194. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_scoping.cpp +0 -0
  195. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_surface.cpp +0 -0
  196. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_tail_calls.cpp +0 -0
  197. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_text.cpp +0 -0
  198. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_transforms.cpp +0 -0
  199. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_unexpected_args.cpp +0 -0
  200. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_use.cpp +0 -0
  201. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.55.0 → openscad_cpp_evaluator-0.55.2}/uv.lock +0 -0
@@ -699,6 +699,17 @@ grep for `ponytail:`.
699
699
  permanent cache miss for any subtree that called `rands()` while resolving, since such a
700
700
  subtree's resolved params aren't a pure function of its own content anymore (the actual `rands()`
701
701
  output also depends on every earlier `rands()` call's position in the whole evaluation's order).
702
+ A second, separate taint — `CSGNode::keyHasClosure`, set by `cacheKey()` itself rather than at
703
+ resolve time — does the same for any subtree holding a **function literal** in its params (the
704
+ `levelset(function (x,y,z) ..., ...)` shape). `canonValue()` can only key a closure by its AST
705
+ node's raw address, which is meaningful for exactly one parse: the next render frees that AST and
706
+ a fresh closure over *different* captured values can be allocated at the very same address, so
707
+ the key silently hits the previous render's geometry — the GUI symptom was an edited script
708
+ rendering its **old** shape until saved a second time. (The address also ignores the captured
709
+ values themselves, so two closures over one literal collide within a single render, which is what
710
+ `tests/test_manifold_cache.cpp` pins down deterministically.) Keying a closure honestly means
711
+ canonicalizing its captured `let_` trail, which is type-erased (`Closure::capturedLet` is a
712
+ `shared_ptr<void>`); until that exists such a subtree simply does not participate in the cache.
702
713
  Builtins are registered in `src/builtins/registry.cpp` as `(name string) -> (ResolveFn,
703
714
  GenerateFn)` pairs — `dispatch.hpp` has the exact reasoning for `GenerateFn`'s `Evaluator&`
704
715
  parameter (provenance-table writes only, no `EvalContext`/dynamic-scope access, so the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.55.0
3
+ Version: 0.55.2
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -25,6 +25,7 @@
25
25
  // losing how the author wrote it (1.500 -> 1.5, 1e3 -> 1000).
26
26
 
27
27
  #include "openscad_cpp_parser/api.hpp"
28
+ #include "openscad_cpp_parser/pretty_print.hpp"
28
29
  #include "openscad_cpp_parser/ast/ast_node.hpp"
29
30
  #include "openscad_cpp_parser/ast/comments.hpp"
30
31
  #include "openscad_cpp_parser/ast/declarations.hpp"
@@ -323,4 +324,11 @@ nb::list parseAstFromString(const std::string& code, bool includeComments) {
323
324
  return astToPy(oscad::getASTFromString(code, includeComments));
324
325
  }
325
326
 
327
+ // The parser already has a pretty-printer -- comment-preserving, idempotent
328
+ // and covered by its own tests -- so an editor's "reformat" needs no second
329
+ // implementation guessing at the same grammar from a token stream.
330
+ std::string formatSourceString(const std::string& code, int indentWidth, bool includeComments) {
331
+ return oscad::toOpenscad(oscad::getASTFromString(code, includeComments), indentWidth);
332
+ }
333
+
326
334
  } // namespace oscadbind
@@ -49,6 +49,7 @@ namespace nb = nanobind;
49
49
  namespace oscadbind {
50
50
  nb::list parseAstFromFile(const std::string& path, bool includeComments);
51
51
  nb::list parseAstFromString(const std::string& code, bool includeComments);
52
+ std::string formatSourceString(const std::string& code, int indentWidth, bool includeComments);
52
53
  } // namespace oscadbind
53
54
 
54
55
  namespace {
@@ -809,6 +810,13 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
809
810
  "safe to keep, share across threads, and outlive the parse.");
810
811
  m.def("parse_ast_string", &oscadbind::parseAstFromString, nb::arg("code"), nb::arg("include_comments") = false,
811
812
  "As parse_ast(), but parses a source string instead of a file.");
813
+ m.def("format_source", &oscadbind::formatSourceString, nb::arg("code"),
814
+ nb::arg("indent") = 4, nb::arg("include_comments") = true,
815
+ "Reformat OpenSCAD source: parse it and print the AST back out. "
816
+ "Wraps over-long argument lists and vectors, puts a modifier's "
817
+ "children on their own indented line, and preserves comments. "
818
+ "Idempotent -- formatting formatted source returns it unchanged. "
819
+ "Raises ParseError if the source does not parse.");
812
820
  m.def("debug_evaluate", &debugEvaluate, nb::arg("path"), nb::arg("viewport_params"), nb::arg("debug_hook"),
813
821
  nb::arg("error_break"), nb::arg("echo_fn"), nb::arg("manifold_cache") = nullptr,
814
822
  nb::arg("return_hook") = nb::none(), nb::arg("fast_continue_signal") = nullptr,
@@ -72,6 +72,21 @@ struct CSGNode {
72
72
  // on first cacheKey() call; every node here is always reached through a
73
73
  // non-const CSGNode&, so no `mutable` is needed to write it.
74
74
  std::optional<std::string> cachedKey;
75
+
76
+ // Set by the same cacheKey() pass that fills cachedKey: true if this
77
+ // node's own params, or any descendant's, hold a function literal.
78
+ //
79
+ // canonValue() can only key a closure by its AST node's raw address,
80
+ // and that address is meaningful for exactly one parse: the next
81
+ // render frees that AST, and a fresh closure over different captured
82
+ // values can be allocated at the very same address -- so the key
83
+ // silently hits the PREVIOUS render's geometry. (It also ignores the
84
+ // captured values themselves, so two closures over one literal share a
85
+ // key within a single render.) Keying a closure honestly would mean
86
+ // canonicalising its captured `let_` trail, which is type-erased; until
87
+ // that exists, such a subtree just does not participate in the cache.
88
+ // See csg_generate.cpp's lookup.
89
+ bool keyHasClosure = false;
75
90
  };
76
91
 
77
92
 
@@ -427,7 +427,8 @@ public:
427
427
  // before its GenerateFn runs -- see csg_generate.cpp.
428
428
  void applyDimensionRules(CSGNode& node);
429
429
  // ruleValue is a DimRule; the enum is file-local to csg_generate.cpp.
430
- void applyDimensionRulesTo(const std::vector<CSGNode*>& children, int ruleValue);
430
+ void applyDimensionRulesTo(const std::vector<CSGNode*>& children, int ruleValue,
431
+ bool keepMixed = false);
431
432
 
432
433
  // Apply `!`: replace the whole tree with the first show_only
433
434
  // subtree, discarding its ancestors and every sibling. See its
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.55.0"
7
+ version = "0.55.2"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -18,7 +18,8 @@ from ._openscad_cpp_evaluator import FastContinueSignal, ManifoldCache
18
18
  __all__ = [
19
19
  "Evaluator", "ColoredBody", "EvalError", "ParseError", "OscObject", "parse", "to_renderable_bodies",
20
20
  "ManifoldCache", "CallSiteProfile", "ProfileResult", "format_csg_tree", "bodies_from_dicts",
21
- "FastContinueSignal", "parse_ast", "parse_ast_string", "check_mesh", "strip_slivers",
21
+ "FastContinueSignal", "parse_ast", "parse_ast_string", "format_source",
22
+ "check_mesh", "strip_slivers",
22
23
  "export_model", "export_extensions",
23
24
  ]
24
25
 
@@ -376,6 +377,24 @@ def parse_ast_string(code: str, include_comments: bool = False) -> list:
376
377
  raise ParseError(str(e)) from e
377
378
 
378
379
 
380
+ def format_source(code: str, indent: int = 4, include_comments: bool = True) -> str:
381
+ """Reformat OpenSCAD source: parse it and print the AST back out.
382
+
383
+ Wraps over-long argument lists and vector literals, puts a modifier's
384
+ children on their own indented line, and preserves comments. Idempotent
385
+ -- formatting already-formatted source returns it unchanged. Raises
386
+ ParseError on a syntax error.
387
+
388
+ This is the parser's own printer (`oscad::toOpenscad`), so an editor
389
+ reformatting a selection does not need a second implementation guessing
390
+ at the same grammar from a token stream.
391
+ """
392
+ try:
393
+ return _ext.format_source(code, indent, include_comments)
394
+ except Exception as e:
395
+ raise ParseError(str(e)) from e
396
+
397
+
379
398
  class _MeshShim:
380
399
  """Duck-types a manifold3d mesh: the four arrays the renderer reads."""
381
400
  __slots__ = ("vert_properties", "tri_verts", "run_original_id", "run_index", "num_prop")
@@ -44,7 +44,15 @@ std::vector<ColoredBody> Evaluator::generateTreeImpl(const std::vector<CSGNode*>
44
44
  // re-deriving results that would just be discarded. Mirrors
45
45
  // generate_tree()'s own cache check exactly.
46
46
  std::optional<std::string> key;
47
- if (manifoldCache_ && !node.uncacheable) key = cacheKey(node);
47
+ if (manifoldCache_ && !node.uncacheable) {
48
+ key = cacheKey(node);
49
+ // A key that serialized a function literal is not a pure
50
+ // function of content -- it embeds a raw AST address that the
51
+ // next parse can hand out again. Dropping the key here forces
52
+ // both the get() below and the put() further down to be
53
+ // skipped. See CSGNode::keyHasClosure.
54
+ if (node.keyHasClosure) key.reset();
55
+ }
48
56
  std::optional<std::vector<ColoredBody>> cached = key ? manifoldCache_->get(*key) : std::nullopt;
49
57
  if (cached) {
50
58
  node.bodies = std::move(*cached);
@@ -163,9 +171,10 @@ void Evaluator::applyDimensionRules(CSGNode& node) {
163
171
  applyDimensionRulesTo(kids, static_cast<int>(rule));
164
172
  }
165
173
 
166
- // The top level is an implicit union, and the reference treats it as one for
167
- // this too: `square(4); cube(1);` warns and keeps only the square.
168
- void Evaluator::applyDimensionRulesTo(const std::vector<CSGNode*>& children, int ruleValue) {
174
+ // `keepMixed` warns about a mixed group but drops nothing -- the top level's
175
+ // behaviour, see generateTree.
176
+ void Evaluator::applyDimensionRulesTo(const std::vector<CSGNode*>& children, int ruleValue,
177
+ bool keepMixed) {
169
178
  const DimRule rule = static_cast<DimRule>(ruleValue);
170
179
  int target = rule == DimRule::Only2D ? 2 : rule == DimRule::Only3D ? 3 : 0;
171
180
  bool warnedMixing = false;
@@ -194,6 +203,16 @@ void Evaluator::applyDimensionRulesTo(const std::vector<CSGNode*>& children, int
194
203
  warn("Mixing 2D and 3D objects is not supported", pos);
195
204
  warnedMixing = true;
196
205
  }
206
+ if (keepMixed) {
207
+ // Kept, not dropped: at the top level there is no single
208
+ // result to build, so nothing downstream can be confused by
209
+ // the other dimension, and the reference's own preview shows
210
+ // both. The warning above still stands -- an export cannot
211
+ // represent the mix -- but silently losing the 2D geometry
212
+ // from the picture is worse than showing it.
213
+ kept.push_back(std::move(b));
214
+ continue;
215
+ }
197
216
  warn(std::string("Ignoring ") + (dim == 3 ? "3D" : "2D") + " child object for " +
198
217
  (target == 3 ? "3D" : "2D") + " operation",
199
218
  pos);
@@ -208,9 +227,19 @@ std::vector<ColoredBody> Evaluator::generateTree(const std::vector<std::unique_p
208
227
  ptrs.reserve(tree.size());
209
228
  for (const std::unique_ptr<CSGNode>& n : tree) ptrs.push_back(n.get());
210
229
  std::vector<ColoredBody> bodies = generateTreeImpl(ptrs);
211
- // The top level is an implicit union; give it the same Group treatment
212
- // every other group node gets, then re-collect what survived.
213
- applyDimensionRulesTo(ptrs, static_cast<int>(DimRule::Group));
230
+ // The top level is an implicit union, so it gets the same Group warning
231
+ // every other group node gets -- but it KEEPS both dimensions.
232
+ //
233
+ // A real group has to pick one: generateCsg builds a single result and
234
+ // switches on whether it holds a body or a section, so a group that
235
+ // changed dimension part-way used to take the process down. The top
236
+ // level builds no such result; it just hands back a list. Dropping there
237
+ // only lost geometry, and it lost it from the picture: the reference's
238
+ // preview draws top-level 2D and 3D side by side (a 2D shape as a thin
239
+ // slab), which is exactly what BOSL2's debug_bezier() of a 2D bezier
240
+ // relies on -- every curve and control arrow it draws is 2D, so beside
241
+ // a 3D sphere the whole debug overlay vanished.
242
+ applyDimensionRulesTo(ptrs, static_cast<int>(DimRule::Group), /*keepMixed=*/true);
214
243
  bodies.clear();
215
244
  for (CSGNode* n : ptrs) {
216
245
  for (const ColoredBody& b : n->bodies) bodies.push_back(b);
@@ -429,10 +429,23 @@ std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& b
429
429
  std::vector<Solid> solids;
430
430
  std::vector<ExportObject> loose;
431
431
 
432
+ // A `flatPreview` body is a top-level 2D shape thin-extruded so it can be
433
+ // SEEN (toRenderableBodies). The top level keeps 2D and 3D side by side
434
+ // for exactly that reason -- but a mesh export of a mixed script must
435
+ // not smuggle a 1e-3-tall slab in beside the real solids, which is what
436
+ // the reference drops there too. A 2D-ONLY script still exports its
437
+ // slab, unchanged: that is the only geometry there is, and dropping it
438
+ // would leave nothing to write.
439
+ bool hasRealSolid = false;
440
+ for (const ColoredBody& cb : bodies) {
441
+ if (!cb.flatPreview && isExportable(cb)) { hasRealSolid = true; break; }
442
+ }
443
+
432
444
  int index = 0;
433
445
  for (const ColoredBody& cb : bodies) {
434
446
  ++index;
435
447
  if (!isExportable(cb)) continue;
448
+ if (cb.flatPreview && hasRealSolid) continue;
436
449
  if (cb.isDisplayOnly()) {
437
450
  // Manifold rejected this one -- an open shell is not a solid --
438
451
  // so it can join no boolean. Its triangles are real geometry the
@@ -93,6 +93,32 @@ std::string canonValue(const Value& v) {
93
93
  v);
94
94
  }
95
95
 
96
+ // True if `v` is a function literal, or a list/object holding one at any
97
+ // depth. Drives CSGNode::keyHasClosure -- see its doc comment for why a
98
+ // closure cannot be keyed.
99
+ bool valueHasClosure(const Value& v) {
100
+ return std::visit(
101
+ [](const auto& val) -> bool {
102
+ using T = std::decay_t<decltype(val)>;
103
+ if constexpr (std::is_same_v<T, ClosurePtr>) {
104
+ return true;
105
+ } else if constexpr (std::is_same_v<T, ListPtr>) {
106
+ if (!val) return false;
107
+ for (const Value& item : val->items)
108
+ if (valueHasClosure(item)) return true;
109
+ return false;
110
+ } else if constexpr (std::is_same_v<T, ObjectPtr>) {
111
+ if (!val) return false;
112
+ for (const auto& [k, vv] : val->items)
113
+ if (valueHasClosure(vv)) return true;
114
+ return false;
115
+ } else {
116
+ return false;
117
+ }
118
+ },
119
+ v);
120
+ }
121
+
96
122
  std::string canonParams(const CSGParams& params) {
97
123
  std::vector<std::pair<std::string, std::string>> entries;
98
124
  entries.reserve(params.size());
@@ -120,13 +146,24 @@ std::string canonParams(const CSGParams& params) {
120
146
  std::string cacheKey(CSGNode& node) {
121
147
  if (node.cachedKey) return *node.cachedKey;
122
148
 
149
+ bool hasClosure = false;
150
+ for (const auto& [k, v] : node.params) {
151
+ if (valueHasClosure(v)) { hasClosure = true; break; }
152
+ }
153
+
123
154
  std::string out = "N(";
124
155
  appendLenPrefixed(out, node.kind);
125
156
  out += node.isBuiltin ? "1" : "0";
126
157
  appendLenPrefixed(out, canonParams(node.params));
127
158
  out += std::to_string(node.children.size()) + "[";
128
- for (const auto& c : node.children) appendLenPrefixed(out, cacheKey(*c));
159
+ for (const auto& c : node.children) {
160
+ appendLenPrefixed(out, cacheKey(*c));
161
+ // A closure anywhere below poisons this key too: it is built from
162
+ // the children's keys.
163
+ hasClosure = hasClosure || c->keyHasClosure;
164
+ }
129
165
  out += "])";
166
+ node.keyHasClosure = hasClosure;
130
167
  node.cachedKey = out;
131
168
  return *node.cachedKey;
132
169
  }
@@ -1,4 +1,6 @@
1
1
  #include "openscad_cpp_evaluator/evaluator.hpp"
2
+ #include "openscad_cpp_evaluator/colored_body.hpp"
3
+ #include "openscad_cpp_evaluator/export.hpp"
2
4
 
3
5
  #include "test_helpers.hpp"
4
6
 
@@ -576,8 +578,11 @@ TEST(MixedDimensions, TransformsAndControlFlowAreGroupsToo) {
576
578
  EXPECT_EQ(dimWarnings("translate([0,0,0]) { square(4); cube(1); }"), expected);
577
579
  EXPECT_EQ(dimWarnings("scale(2) { square(4); cube(1); }"), expected);
578
580
  EXPECT_EQ(dimWarnings("color(\"red\") { square(4); cube(1); }"), expected);
579
- EXPECT_EQ(dimWarnings("for (i = [0:0]) { square(4); cube(1); }"), expected);
580
- EXPECT_EQ(dimWarnings("if (true) { square(4); cube(1); }"), expected);
581
+ // for/if are transparent -- their children land at the top level, so
582
+ // they get the top level's keep-both treatment rather than a group's.
583
+ const std::vector<std::string> topLevel{"WARNING: Mixing 2D and 3D objects is not supported"};
584
+ EXPECT_EQ(dimWarnings("for (i = [0:0]) { square(4); cube(1); }"), topLevel);
585
+ EXPECT_EQ(dimWarnings("if (true) { square(4); cube(1); }"), topLevel);
581
586
  EXPECT_EQ(dimWarnings("hull() { square(4); cube(1); }"), expected);
582
587
  }
583
588
 
@@ -603,14 +608,47 @@ TEST(MixedDimensions, RoofWarnsForNeither) {
603
608
  EXPECT_TRUE(dimWarnings("roof() { square(4); cube(1); }").empty());
604
609
  }
605
610
 
606
- TEST(MixedDimensions, TopLevelIsAGroupAsWell) {
607
- // The top level is an implicit union, and gets the same treatment.
611
+ TEST(MixedDimensions, TopLevelWarnsButKeepsBothDimensions) {
612
+ // The top level warns like any other group, but drops NOTHING. A real
613
+ // group has to pick one dimension because generateCsg builds a single
614
+ // result from it; the top level just hands back a list, and the
615
+ // reference's own preview draws 2D and 3D side by side. Dropping here
616
+ // silently lost the whole of a BOSL2 debug overlay (debug_bezier() of a
617
+ // 2D bezier is entirely 2D) whenever a 3D shape shared the script.
608
618
  EXPECT_EQ(dimWarnings("square(4); cube(1);"),
609
- (std::vector<std::string>{"WARNING: Ignoring 3D child object for 2D operation",
610
- "WARNING: Mixing 2D and 3D objects is not supported"}));
619
+ (std::vector<std::string>{"WARNING: Mixing 2D and 3D objects is not supported"}));
611
620
  Evaluated e = evalSrc("square(4); cube(1);");
612
- ASSERT_EQ(e.bodies.size(), 1u);
613
- EXPECT_TRUE(e.bodies[0].section.has_value());
621
+ ASSERT_EQ(e.bodies.size(), 2u);
622
+ EXPECT_TRUE(e.bodies[0].section.has_value()) << "the 2D shape survived";
623
+ EXPECT_TRUE(e.bodies[1].body.has_value()) << "and so did the 3D one";
624
+ }
625
+
626
+ TEST(MixedDimensions, ExportDropsTheFlatSlabWhenRealSolidsArePresent) {
627
+ // The top level keeps a 2D shape so it can be SEEN, thin-extruded by
628
+ // toRenderableBodies. A mesh export must not smuggle that 1e-3-tall slab
629
+ // in beside the real solids -- the reference drops it there too.
630
+ Evaluated e = evalSrc("square(4); cube(1);");
631
+ const std::vector<ColoredBody> renderable = toRenderableBodies(e.bodies);
632
+ ASSERT_EQ(renderable.size(), 2u) << "both are still drawn";
633
+
634
+ const std::vector<ExportObject> objects = splitBodiesForExport(renderable, nullptr);
635
+ ASSERT_EQ(objects.size(), 1u) << "but only the cube is exported";
636
+ // The cube is a unit tall; the dropped slab would have been 1e-3.
637
+ float zmin = 1e9f, zmax = -1e9f;
638
+ for (size_t i = 2; i < objects[0].verts.size(); i += 3) {
639
+ zmin = std::min(zmin, objects[0].verts[i]);
640
+ zmax = std::max(zmax, objects[0].verts[i]);
641
+ }
642
+ EXPECT_NEAR(zmax - zmin, 1.0f, 1e-5f);
643
+ }
644
+
645
+ TEST(MixedDimensions, ExportKeepsTheFlatSlabWhenItIsTheOnlyGeometry) {
646
+ // A 2D-only script has nothing else to write, so its slab still exports
647
+ // -- unchanged behaviour, and the only reason the filter is conditional.
648
+ Evaluated e = evalSrc("square(4);");
649
+ const std::vector<ExportObject> objects =
650
+ splitBodiesForExport(toRenderableBodies(e.bodies), nullptr);
651
+ EXPECT_EQ(objects.size(), 1u);
614
652
  }
615
653
 
616
654
  TEST(MixedDimensions, UniformChildrenAreUntouched) {
@@ -121,6 +121,65 @@ TEST(ManifoldCacheUncacheable, RandsBeforeGeometryInModuleBodyTaintsSplicedSibli
121
121
  EXPECT_TRUE(e.tree[0]->uncacheable);
122
122
  }
123
123
 
124
+ // -- closure taint ---------------------------------------------------------
125
+
126
+ TEST(ManifoldCacheClosure, PlainNodeHasNoClosureTaint) {
127
+ Evaluated e = evalSrc("cube(5);");
128
+ ASSERT_EQ(e.tree.size(), 1u);
129
+ cacheKey(*e.tree[0]);
130
+ EXPECT_FALSE(e.tree[0]->keyHasClosure);
131
+ }
132
+
133
+ TEST(ManifoldCacheClosure, FunctionLiteralParamTaintsTheNode) {
134
+ Evaluated e = evalSrc(
135
+ "levelset(function (x,y,z) x*x+y*y+z*z-100,"
136
+ " bounds=[[-15,-15,-15],[15,15,15]], edge=5);");
137
+ ASSERT_EQ(e.tree.size(), 1u);
138
+ cacheKey(*e.tree[0]);
139
+ EXPECT_TRUE(e.tree[0]->keyHasClosure);
140
+ }
141
+
142
+ TEST(ManifoldCacheClosure, TaintPropagatesUpThroughATransform) {
143
+ // The parent's key is built from the child's, so a closure below is
144
+ // just as unusable there.
145
+ Evaluated e = evalSrc(
146
+ "translate([1,0,0]) levelset(function (x,y,z) x*x+y*y+z*z-100,"
147
+ " bounds=[[-15,-15,-15],[15,15,15]], edge=5);");
148
+ ASSERT_EQ(e.tree.size(), 1u);
149
+ EXPECT_EQ(e.tree[0]->kind, "translate");
150
+ cacheKey(*e.tree[0]);
151
+ EXPECT_TRUE(e.tree[0]->keyHasClosure);
152
+ }
153
+
154
+ // The bug this taint exists for, at full size: both closures come from the
155
+ // SAME function literal (so canonValue's "F<pointer>" is byte-identical)
156
+ // but capture different `r`. Every other cache-key input matches too, so
157
+ // before the taint the second levelset() was served the first one's
158
+ // geometry and both spheres came out radius 10.
159
+ //
160
+ // Deterministic on purpose: one evaluation, one AST, so the two closures
161
+ // genuinely share a node address rather than relying on the allocator
162
+ // handing back a freed one.
163
+ TEST(ManifoldCacheClosure, TwoClosuresOverOneLiteralAreNotServedEachOthersGeometry) {
164
+ auto cache = std::make_shared<ManifoldCache>();
165
+ Evaluated e = evalSrcWithCache(
166
+ "function mk(r) = function (x,y,z) x*x+y*y+z*z-r*r;\n"
167
+ "levelset(mk(10), bounds=[[-15,-15,-15],[15,15,15]], edge=2);\n"
168
+ "translate([40,0,0])\n"
169
+ " levelset(mk(5), bounds=[[-15,-15,-15],[15,15,15]], edge=2);\n",
170
+ cache);
171
+
172
+ ASSERT_EQ(e.bodies.size(), 2u);
173
+ ASSERT_TRUE(e.bodies[0].body.has_value());
174
+ ASSERT_TRUE(e.bodies[1].body.has_value());
175
+ const double volBig = e.bodies[0].body->Volume();
176
+ const double volSmall = e.bodies[1].body->Volume();
177
+ // Coarse sampling, so compare the two against each other rather than
178
+ // against 4/3*pi*r^3: r=10 vs r=5 is an 8x volume ratio.
179
+ EXPECT_GT(volBig, 2.0 * volSmall)
180
+ << "the second closure was served the first one's cached geometry";
181
+ }
182
+
124
183
  // -- cache reuse -----------------------------------------------------------
125
184
 
126
185
  TEST(ManifoldCache, CacheHitSkipsRegeneratingTheSubtree) {
@@ -351,6 +351,43 @@ def test_parse_ast_raises_parse_error_on_bad_syntax():
351
351
  raise AssertionError("expected ParseError")
352
352
 
353
353
 
354
+ def test_format_source_round_trips_and_is_idempotent():
355
+ from openscad_cpp_evaluator import format_source
356
+
357
+ out = format_source("module thing(a=1,b=[1,2,3]){translate([0,0,1])cube([a,a,a]);}\n")
358
+ assert "module thing(a=1, b=[1, 2, 3])" in out, out
359
+ # A modifier's children go on their own indented line.
360
+ assert "translate([0, 0, 1])\n cube([a, a, a]);" in out, out
361
+ # Formatting formatted source changes nothing: the printer is the
362
+ # parser's own, so its output parses back to the same tree.
363
+ assert format_source(out) == out
364
+
365
+
366
+ def test_format_source_keeps_comments():
367
+ from openscad_cpp_evaluator import format_source
368
+
369
+ out = format_source("// keep me\ncube(1);\n")
370
+ assert "// keep me" in out, out
371
+ # ...and can be told not to.
372
+ assert "// keep me" not in format_source("// keep me\ncube(1);\n", include_comments=False)
373
+
374
+
375
+ def test_format_source_honours_the_indent_width():
376
+ from openscad_cpp_evaluator import format_source
377
+
378
+ two = format_source("module m(){cube(1);}\n", indent=2)
379
+ assert "\n cube(1);" in two, two
380
+
381
+
382
+ def test_format_source_raises_parse_error_on_bad_input():
383
+ from openscad_cpp_evaluator import ParseError, format_source
384
+ try:
385
+ format_source("module {{{")
386
+ except ParseError:
387
+ return
388
+ raise AssertionError("expected ParseError")
389
+
390
+
354
391
  def main():
355
392
  tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
356
393
  failures = []