openscad-cpp-evaluator 0.40.0__tar.gz → 0.50.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.40.0 → openscad_cpp_evaluator-0.50.0}/CLAUDE.md +136 -6
  2. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/CMakeLists.txt +13 -1
  3. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/PKG-INFO +1 -1
  4. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/bindings/module.cpp +34 -11
  5. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +10 -2
  6. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/ast/expression.cpp +4 -0
  7. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +4 -2
  8. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +3 -1
  9. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +13 -0
  10. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/bytecode.hpp +15 -0
  11. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/csg_node.hpp +2 -0
  12. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/evaluator.hpp +60 -1
  13. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/export.hpp +7 -0
  14. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/value.hpp +1 -16
  15. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/pyproject.toml +1 -1
  16. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/python/openscad_cpp_evaluator/__init__.py +19 -2
  17. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/CMakeLists.txt +8 -0
  18. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/booleans.cpp +11 -6
  19. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/builtins.hpp +5 -0
  20. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/control.cpp +5 -7
  21. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/function_builtins.cpp +354 -7
  22. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/registry.cpp +4 -1
  23. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/topology.cpp +249 -0
  24. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/bytecode_compiler.cpp +56 -17
  25. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/bytecode_vm.cpp +28 -5
  26. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/csg_resolve.cpp +1 -0
  27. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/eval_context.cpp +28 -0
  28. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/export.cpp +89 -2
  29. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/expr_eval.cpp +47 -5
  30. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/stmt_eval.cpp +0 -2
  31. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/user_calls.cpp +136 -25
  32. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/value.cpp +6 -18
  33. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/CMakeLists.txt +2 -0
  34. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_booleans.cpp +214 -0
  35. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_bytecode_compiler.cpp +7 -7
  36. openscad_cpp_evaluator-0.50.0/tests/test_children_separate.cpp +415 -0
  37. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_control_flow.cpp +41 -16
  38. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_export.cpp +85 -0
  39. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_expr_eval.cpp +24 -11
  40. openscad_cpp_evaluator-0.50.0/tests/test_feature_detection.cpp +162 -0
  41. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_function_builtins.cpp +250 -11
  42. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_use.cpp +6 -3
  43. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/.github/workflows/ci.yml +0 -0
  44. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/.github/workflows/wheels.yml +0 -0
  45. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/.gitignore +0 -0
  46. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/.gitmodules +0 -0
  47. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/LICENSE +0 -0
  48. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/README.md +0 -0
  49. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/bindings/CMakeLists.txt +0 -0
  50. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/bindings/ast_to_py.cpp +0 -0
  51. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/cmake/embed_font.cmake +0 -0
  52. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/examples/CMakeLists.txt +0 -0
  53. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/examples/eval_perf_benchmark.cpp +0 -0
  54. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/examples/manifold_cache_reuse.cpp +0 -0
  55. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/examples/minimal_debugger.cpp +0 -0
  56. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  57. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/.gitignore +0 -0
  58. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  59. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  60. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/README.md +0 -0
  61. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  62. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  63. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  64. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  65. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  66. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  67. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  68. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  69. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  70. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  71. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  72. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  73. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  74. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  75. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  76. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  77. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  78. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  79. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  80. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  81. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  82. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  83. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  84. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  85. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  86. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  87. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  88. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  89. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  90. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  91. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  92. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  93. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  94. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  95. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  96. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  97. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  98. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  99. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  100. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  101. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  102. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  103. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  104. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  105. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  106. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  107. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  108. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  109. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  110. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  111. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  112. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  113. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  114. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  115. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  116. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  117. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  118. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  119. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  120. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  121. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  122. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  123. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  124. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  125. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  126. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  127. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  128. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  129. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  130. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  131. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  132. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  133. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  134. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  135. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  136. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  137. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  138. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  139. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  140. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  141. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  142. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  143. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  144. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  145. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  146. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/resources/fonts/LICENSE +0 -0
  147. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  148. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/call_args.cpp +0 -0
  149. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/color.cpp +0 -0
  150. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/dxf_dim.cpp +0 -0
  151. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/extrude.cpp +0 -0
  152. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/import.cpp +0 -0
  153. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/modifiers.cpp +0 -0
  154. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/primitives_2d.cpp +0 -0
  155. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/primitives_3d.cpp +0 -0
  156. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/roof.cpp +0 -0
  157. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/segments.cpp +0 -0
  158. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/surface.cpp +0 -0
  159. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/text.cpp +0 -0
  160. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/builtins/transforms.cpp +0 -0
  161. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/colored_body.cpp +0 -0
  162. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/csg_generate.cpp +0 -0
  163. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/css_colors.cpp +0 -0
  164. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/debug/debug_repl.cpp +0 -0
  165. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/debug_profile.cpp +0 -0
  166. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/eval_error.cpp +0 -0
  167. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/eval_use.cpp +0 -0
  168. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/import/dxf_import.cpp +0 -0
  169. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/import/mesh_import.cpp +0 -0
  170. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/import/surface_load.cpp +0 -0
  171. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/import/svg_import.cpp +0 -0
  172. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/manifold_cache.cpp +0 -0
  173. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/measure_geometry.cpp +0 -0
  174. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/mesh_check.cpp +0 -0
  175. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/text/font_match.cpp +0 -0
  176. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/text/freetype_font_provider.cpp +0 -0
  177. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/text/text_metrics.cpp +0 -0
  178. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/src/zip_stored.cpp +0 -0
  179. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_cli.cpp +0 -0
  180. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_csg_tree.cpp +0 -0
  181. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_debug_hooks.cpp +0 -0
  182. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_dxf_svg_import.cpp +0 -0
  183. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_eval_context.cpp +0 -0
  184. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_extrude_roof.cpp +0 -0
  185. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_helpers.hpp +0 -0
  186. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_import_export.cpp +0 -0
  187. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_manifold_cache.cpp +0 -0
  188. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_mesh_check.cpp +0 -0
  189. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_multi_color_merge.cpp +0 -0
  190. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_primitives.cpp +0 -0
  191. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_profiling.cpp +0 -0
  192. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_python_bindings.py +0 -0
  193. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_render_expr.cpp +0 -0
  194. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_scope_trail.cpp +0 -0
  195. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_scoping.cpp +0 -0
  196. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_surface.cpp +0 -0
  197. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_tail_calls.cpp +0 -0
  198. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_text.cpp +0 -0
  199. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_transforms.cpp +0 -0
  200. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_unexpected_args.cpp +0 -0
  201. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.40.0 → openscad_cpp_evaluator-0.50.0}/uv.lock +0 -0
@@ -45,9 +45,13 @@ comprehension clause kinds, user function calls (recursion, defaults, `FunctionL
45
45
  user module calls (`children()`/`children(N)`, closure detection via `callCtxFor`), list
46
46
  indexing/swizzle member access; the full math/string/list/type-check builtin *function* dispatch
47
47
  (`sin`/`cos`/`pow`/`search`/`lookup`/`chr`/`ord`/`is_*`/`object`/`has_key`/etc, ~43 names — a
48
- completely separate namespace/precedence rule from the builtin *modules* in `registry.cpp`: a
49
- builtin function name always wins over a same-named user function, checked *before* the
50
- user-function lookup), STL/OBJ/OFF/3MF `import()` in both module (geometry) and expression
48
+ same precedence rule as the builtin *modules* in `registry.cpp`: a user-defined function
49
+ **shadows** a same-named builtin, looked up *before* the builtin table. This was backwards until
50
+ 0.47.0 — the port copied the old Python evaluator's `_eval_function_call` order, which had it
51
+ wrong; OpenSCAD 2026.02.01 echoes 999 for `function abs(x)=999; abs(-3)` and we echoed 3. Fixing
52
+ it needs **both** resolution sites: `evalFunctionCall` (`user_calls.cpp`) and the compiler's
53
+ call-site resolution (`bytecode_compiler.cpp`), which bakes builtin-vs-user in at compile time,
54
+ plus gating the silent `is_undef` probe on the call actually resolving to the builtin), STL/OBJ/OFF/3MF `import()` in both module (geometry) and expression
51
55
  (VNF/JSON) contexts, and 3MF read/write; (Phase 6) `hull()`/`minkowski()` (transparent-splice
52
56
  CSG-tree shape like union/difference/intersection, role-split via the shared `splitByRole`),
53
57
  `linear_extrude()`/`rotate_extrude()`/`projection(cut=)`, and `roof()`; and (Phase 7) `offset()`,
@@ -441,6 +445,111 @@ handling in arithmetic, the closure-detection rule for `call_ctx` vs `child_ctx`
441
445
  verbatim port, not an independent design. Deliberate divergences are called out in code comments —
442
446
  grep for `ponytail:`.
443
447
 
448
+ - **Feature detection** — `$_SUPPORTED_FEATURE` (just `true`; names the CAPABILITY rather than the
449
+ vendor, so any evaluator adding `supported_feature()` is meant to set it, and checking it first
450
+ solves the bootstrapping problem of calling a function you don't know exists. Written
451
+ `!is_undef($_SUPPORTED_FEATURE) && supported_feature(...)` the guard is **silent** in OpenSCAD --
452
+ `is_undef()` is the one way to read an unknown variable there without a warning, and `&&`
453
+ short-circuits past the unknown call; verified against the reference), `$_BELFRYSCAD` (seeded in `EvalContext::makeRoot`, `src/eval_context.cpp`, as
454
+ `[major, minor, patch]` from `OSCAD_EVAL_VERSION_*` compile definitions that the top-level
455
+ `CMakeLists.txt` parses out of `pyproject.toml`, so wheel and local builds cannot disagree and a
456
+ bump needs one edit) and `supported_feature("name")` (`featureLevels()` in
457
+ `src/builtins/function_builtins.cpp`, returning a LEVEL so a feature whose semantics change can
458
+ be told from its earlier self; 0 for unknown names and non-string arguments, deliberately
459
+ silent so probing for a future feature is safe). Both exist because OpenSCAD **silently ignores**
460
+ unknown arguments -- `children(separate=true)` renders the wrong shape there with no warning --
461
+ and both read as `undef` in OpenSCAD, so a guard written against them works in both.
462
+
463
+ - **`levelset(field, bounds, isovalue)`** (`src/builtins/topology.cpp`, feature name `levelset`) —
464
+ a solid from a grid of sampled values: `field[i][j][k]` with i,j,k mapping to x,y,z. Wraps
465
+ `Manifold::LevelSet` with a C++ trilinear-sampling lambda; no new Manifold API.
466
+ **The field is a grid OR a `function(x,y,z)`, and the two trade off against each other** —
467
+ neither is simply better:
468
+
469
+ | | sampling | parallel | accuracy | ceiling |
470
+ |---|---|---|---|---|
471
+ | grid `field[i][j][k]` | 0.41 µs | **yes** | capped by the grid | ~550 MB at 200³ |
472
+ | `function(x,y,z)` | 0.96 µs | **no** | Manifold snaps toward the true surface | none |
473
+
474
+ Measured at matched resolution against the analytic sphere: **50³ grid 0.23 s (−0.244%) vs
475
+ function 0.32 s (−0.103%); 100³ grid 1.06 s (−0.059%) vs function 1.91 s (−0.025%)**. So the
476
+ function form is ~1.5× slower and ~2.4× more accurate. Both still beat BOSL2.
477
+ `canParallel` is the crux and is **not a tuning knob**: true for a grid because the lambda is
478
+ pure C++, false for a function because Manifold warns that parallel policies "will crash
479
+ language runtimes with runtime locks that expect to not be called back by unregistered threads".
480
+ The function form also needs `edge=` explicitly — there is no grid to infer spacing from and the
481
+ cost is cubic in it, so guessing would silently produce either a useless mesh or a ten-minute
482
+ one.
483
+ Measured against BOSL2's `isosurface.scad` (217 KB of marching cubes in script), same field:
484
+ **50³ 0.26 s vs 2.07 s (8.0×), 100³ 1.15 s vs 14.59 s (12.7×)**.
485
+ The trade is **memory**: ~67 bytes/cell, so 200³ ≈ 550 MB is the practical ceiling. A callback
486
+ samples lazily and has none.
487
+ **Accuracy is bounded by the grid, not by Manifold.** It samples on a body-centred cubic lattice
488
+ (`sdf.cpp:440-446`) so a plain cubic grid does not line up and the lambda interpolates — same
489
+ accuracy as grid-based marching cubes in script, worse than `LevelSet` given a true SDF. Hence
490
+ `tolerance = -1` is forced: a positive value makes Manifold do extra evaluations per vertex that
491
+ would only re-interpolate data already used. `edgeLength` defaults to the grid spacing for the
492
+ same reason.
493
+ Calling the field function from the generate pass has no live `EvalContext`, so a root is built
494
+ from the closure's own scope — `$`-variables sit at their defaults inside it.
495
+ Sign convention: Manifold takes positive as inside; a distance field is the other way round, so
496
+ the default flips it and `invert=true` flips back.
497
+ Two behaviours that look alike and are opposite: an isovalue **nothing reaches** gives empty
498
+ geometry; one **everything satisfies** gives the whole bounding box. Both are tested.
499
+
500
+ - **`linear_solve(A, b)`** (`src/builtins/function_builtins.cpp`, feature name `linear-solve`) —
501
+ returns `.x` (solution), `.det`, `.singular`. Dispatches on shape:
502
+
503
+ | shape | method | `.x` | `.det` |
504
+ |---|---|---|---|
505
+ | m = n | pivoted **LU** | the solution | product of U's diagonal, signed by row swaps |
506
+ | m > n | **Householder QR** | least squares | `undef` |
507
+ | m < n | **QR of Aᵀ** | minimum norm | `undef` |
508
+
509
+ Q is never formed. The reflectors are stored below the diagonal (v[0]
510
+ normalised to 1, scale in `taus`) and applied in forward order for `Qᵀb`,
511
+ reverse order for `Qy` — which is why this is far smaller and quicker than
512
+ BOSL2, whose QR materialises an m×m Q *and* an n×n permutation matrix and
513
+ then multiplies by it. **Minimum norm** comes from padding `y` with zeros to
514
+ length n before applying Q; a residual check alone will NOT catch getting
515
+ that wrong, since any solution passes it — assert the norm.
516
+ Rank deficiency without column pivoting cannot distinguish rank-deficient
517
+ from very ill-conditioned. It is a relative-tolerance heuristic on the
518
+ diagonal; say so rather than implying it is exact.
519
+
520
+ **Named to collide, deliberately.** BOSL2 defines `linear_solve()`, and since
521
+ 0.47.0 a user function *shadows* a builtin — so this is unreachable from any
522
+ script that includes BOSL2 until BOSL2 drops its own. That is the intended
523
+ path (BelfrySCAD's author maintains BOSL2); callers then gain a `.x`, since
524
+ BOSL2's returns the bare solution and `[]` for singular. **Do not "fix" the
525
+ shadowing to make this reachable** — the shadowing is the correct, reference-
526
+ matching behaviour, and was itself a bug fix.
527
+
528
+ Measured per call, BOSL2 load subtracted, enough repetitions to clear noise
529
+ (harness checked: 200/2000/20000 reps at n=64 give 43/45/43 µs):
530
+
531
+ | n | BOSL2 `linear_solve` | ours | | n | BOSL2 `determinant` | ours |
532
+ |---:|---:|---:|---:|---:|---:|---:|
533
+ | 4 | 280 µs | 1.5 µs | 187× | 10 | 3.17 s | 0.33 s |
534
+ | 16 | 2.58 ms | 4.0 µs | 644× | 11 | 31.49 s | 0.33 s |
535
+ | 64 | 42.5 ms | 43 µs | 988× | | *cofactor expansion, O(n!)* | |
536
+
537
+ **Do not repeat the claim this replaces**: that BOSL2's solve "is not the
538
+ problem (128×128 in ~0.18s)". That was one-shot, with the solve lost inside
539
+ BOSL2's own load time. Keep the comparison fair though — BOSL2 does QR where
540
+ the square path here does LU, and it validates arguments every call.
541
+ Also correctness, not only speed: BOSL2's singularity test is a fixed
542
+ **absolute** `1e-9` on R's diagonal, unscaled by ‖A‖, so `2*I` scaled by
543
+ 1e-10 is called singular there and solves correctly here.
544
+
545
+ **Profiling that shaped the scope** (don't redo it): NURBS *evaluation* is
546
+ 0.6% linear algebra — basis eval 44%, type checks 25%. NURBS *interpolation*
547
+ is 35% at n=400, and every hot solve is **square**, so the QR paths are a
548
+ capability, not a measured win. `plane_intersection` (2×3, underdetermined)
549
+ is 0.1% of a `rounded_prism` model. The recurring cost across every workload
550
+ profiled is BOSL2's **argument type-checking**: 25–45%, millions of
551
+ `is_finite`/`is_num`/`is_nan` calls.
552
+
444
553
  - `include/openscad_cpp_evaluator/value.hpp`, `src/value.cpp` — the `Value` variant (OpenSCAD's
445
554
  dynamic value type) and its free-function arithmetic/comparison helpers. See the plan's §1 for
446
555
  the type-mapping table and why there's no numpy-equivalent library.
@@ -481,7 +590,19 @@ grep for `ponytail:`.
481
590
  lexically nested inside an already-active call," picking `childCtx()` vs `callCtx()`
482
591
  accordingly; read its doc comment before touching it, it's the second-trickiest mechanism in this
483
592
  codebase after the CSG tree stack), `evalUserModule`/`evalUserFunction`/`evalFunctionLiteral`,
484
- and `builtinChildren` (children()/children(N), deferred evaluation). `evalFunctionCall`'s
593
+ and `builtinChildren` (children()/children(N), deferred evaluation).
594
+ **`children(..., separate=true)`** is a BelfrySCAD extension handled *before* any of that, by
595
+ `expandChildStatements`: a block is read with each separating forward replaced by one synthetic
596
+ `children(k)` statement per child it selects (`makeChildrenIndexCall`, owned by
597
+ `syntheticNodes_` and cleared with `idToNode` at `resolveTreeImpl` entry). Everything the
598
+ feature does follows from their being real statements -- `$children` counts them,
599
+ `children(i)` indexes them, and `union`/`difference`/`intersection` give each its own operand
600
+ group -- with no flag on `CSGNode` and nothing to leak. Called from `buildModuleChildCtx`,
601
+ `resolveCsg`, `resolveIntersectionFor`, and the VM's `Op::CsgGroupChildren`, which exists
602
+ because the compiler fixes group boundaries long before `$children` (and so the expansion size)
603
+ is knowable; it shares `expandSeparatingChildren` with the interpreter so the engines cannot
604
+ drift. Synthetic calls carry the author's own `Position` and no scope, so they resolve in the
605
+ evaluating context (`evalChildren`'s `runAll`). `evalFunctionCall`'s
485
606
  precedence order (checked in this exact sequence): `import` (special-cased, its own
486
607
  module/expression-context split) → `isBuiltinFunctionName` (function_builtins.hpp — always wins
487
608
  if true) → user function lookup → function-literal *value* probe → "unknown function" warning.
@@ -684,8 +805,17 @@ grep for `ponytail:`.
684
805
  — skip it and the mesh comes out non-manifold at the seam). Self-contained 2D geometry math here
685
806
  (`dot2`/`len2`/`norm2`, the parabola-canonical-frame rotation in `discretizeArc`) exists nowhere
686
807
  else in this codebase.
687
- - `include/openscad_cpp_evaluator/export.hpp`, `src/export.cpp` — binary STL, OBJ, OFF, and 3MF
688
- writers (compose every body into one solid first; 3MF via `zip_stored.hpp`'s `writeDeflateZip`).
808
+ - `include/openscad_cpp_evaluator/export.hpp`, `src/export.cpp` — the writers for all eight
809
+ formats `exportExtensions()` lists: 3MF, AMF, STL, OBJ, OFF, PLY, VRML, X3D (3MF via
810
+ `zip_stored.hpp`'s `writeDeflateZip`). **`exportExtensions()` is the one source of truth for
811
+ which formats exist** — the Python facade and BelfrySCAD's dialog/CLI all ask it rather than
812
+ restating the list, because three hand-written copies had already drifted far enough that `.off`
813
+ could be written by neither the GUI nor the CLI while every test passed.
814
+ Colour is carried by 3MF, AMF, OBJ (companion `.mtl`), PLY, VRML and X3D; STL and OFF compose
815
+ every body into one solid and store geometry only. **AMF** puts colour on the `<volume>` rather
816
+ than the face, so an object whose triangles are not all one colour is written as one volume per
817
+ colour — per-triangle `<color>` is legal in the spec but poorly supported by the slicers that are
818
+ the audience for it.
689
819
  `colored_body.hpp`/`.cpp`'s `toRenderableBodies()` (thin-extrudes any top-level 2D-only result to
690
820
  a 1e-3-unit-tall `flatPreview` Manifold) must run on a body list before handing it to any of these
691
821
  writers — none of them look at `ColoredBody::section` at all, only `.body`. `tools/cli/cli_lib.cpp`
@@ -1,5 +1,17 @@
1
1
  cmake_minimum_required(VERSION 3.24)
2
- project(openscad_cpp_evaluator CXX)
2
+
3
+ # Read the version straight out of pyproject.toml rather than repeating it
4
+ # here. It reaches C++ as OSCAD_EVAL_VERSION_{MAJOR,MINOR,PATCH} (see
5
+ # src/CMakeLists.txt), which is what seeds $_BELFRYSCAD -- so a released
6
+ # wheel and a local `cmake --build` always report the same numbers, and a
7
+ # version bump needs one edit, not three.
8
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/pyproject.toml" _oscad_pyproject)
9
+ if(NOT _oscad_pyproject MATCHES "\nversion = \"([0-9]+)\.([0-9]+)\.([0-9]+)\"")
10
+ message(FATAL_ERROR "could not read version = \"X.Y.Z\" from pyproject.toml")
11
+ endif()
12
+ set(OSCAD_EVAL_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
13
+
14
+ project(openscad_cpp_evaluator VERSION ${OSCAD_EVAL_VERSION} LANGUAGES CXX)
3
15
 
4
16
  # On Apple Silicon, warn (don't fail -- a universal or intentionally-x86_64
5
17
  # build is legitimate) if the effective target architecture is x86_64 while
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.40.0
3
+ Version: 0.50.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -385,17 +385,36 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
385
385
  {
386
386
  nb::gil_scoped_release rel;
387
387
  auto logFn = [&echoes](const std::string& m) { echoes.push_back(m); };
388
- std::vector<std::unique_ptr<oscad::ASTNode>> ast = oscad::getASTFromFile(path);
389
- oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(ast, path, logFn);
390
- oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile);
391
- oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
392
- bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp));
393
- collectIdSpans(ev, idSpans);
394
- csgTree = std::move(ev.csgTree);
395
- profileResult = std::move(ev.profileResult);
396
- {
397
- nb::gil_scoped_acquire g; // building Python objects needs the GIL back
398
- std::tie(dyn, dynExplicit) = dynStateToPy(ctx);
388
+ try {
389
+ std::vector<std::unique_ptr<oscad::ASTNode>> ast = oscad::getASTFromFile(path);
390
+ oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(ast, path, logFn);
391
+ oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile);
392
+ oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
393
+ bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp));
394
+ collectIdSpans(ev, idSpans);
395
+ csgTree = std::move(ev.csgTree);
396
+ profileResult = std::move(ev.profileResult);
397
+ {
398
+ nb::gil_scoped_acquire g; // building Python objects needs the GIL back
399
+ std::tie(dyn, dynExplicit) = dynStateToPy(ctx);
400
+ }
401
+ } catch (const std::exception& e) {
402
+ // `echoes` is a local, so letting this unwind past here loses
403
+ // every echo and warning the script produced BEFORE it failed --
404
+ // which is exactly the output you want when something fails. The
405
+ // batching that makes them cheap (no GIL round-trip per line) is
406
+ // also what made them disposable.
407
+ //
408
+ // Carry them out on the exception itself: args = (message,
409
+ // [echo, ...]). The facade replays them through echo_fn and then
410
+ // re-raises. Anything that only looks at str(e) is unaffected,
411
+ // since args[0] is still the formatted diagnostic.
412
+ nb::gil_scoped_acquire g;
413
+ nb::list partial;
414
+ for (const std::string& s : echoes) partial.append(s);
415
+ nb::object args = nb::make_tuple(nb::str(e.what()), partial);
416
+ PyErr_SetObject(PyExc_RuntimeError, args.ptr());
417
+ throw nb::python_error();
399
418
  }
400
419
  }
401
420
 
@@ -767,6 +786,10 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
767
786
  "logging them. Raises RuntimeError when there is no geometry, the format is unknown, or the "
768
787
  "file cannot be opened.");
769
788
 
789
+ m.def("export_extensions", []() { return oscadeval::exportExtensions(); },
790
+ "The file extensions export_model understands, dot-prefixed. Read from the C++ writer table "
791
+ "rather than restated here, so the Python side cannot drift from what can actually be written.");
792
+
770
793
  m.def("evaluate", &evaluate, nb::arg("path"), nb::arg("viewport_params"), nb::arg("manifold_cache") = nullptr,
771
794
  nb::arg("profile") = false,
772
795
  "Evaluate a .scad file; return (bodies, echoes, id_to_node, csg_tree, profile_result, dyn, dyn_explicit).");
@@ -80,14 +80,22 @@ public:
80
80
  class RangeLiteral : public Primary {
81
81
  public:
82
82
  RangeLiteral(Position position, std::unique_ptr<Expression> start, std::unique_ptr<Expression> end,
83
- std::unique_ptr<Expression> step)
83
+ std::unique_ptr<Expression> step, bool implicitStep = false)
84
84
  : Primary(NodeKind::RangeLiteral, std::move(position)), start(std::move(start)), end(std::move(end)),
85
- step(std::move(step)) {}
85
+ step(std::move(step)), implicitStep(implicitStep) {}
86
86
 
87
87
  std::unique_ptr<Expression> start;
88
88
  std::unique_ptr<Expression> end;
89
89
  std::unique_ptr<Expression> step;
90
90
 
91
+ // True when the source wrote the two-argument form `[a:b]` and the step
92
+ // node below is the literal 1.0 this parser synthesized for it. Consumers
93
+ // that only need the value can ignore this and read `step` as always
94
+ // present; it exists because "the author did not choose a step" is not
95
+ // recoverable from the synthesized node, and the evaluator's backwards-range
96
+ // warning fires only for that case (an explicit step is taken as deliberate).
97
+ bool implicitStep = false;
98
+
91
99
  std::string toString() const override;
92
100
  void buildScope(Scope& parentScope) override;
93
101
  };
@@ -22,6 +22,10 @@ std::string NumberLiteral::toString() const {
22
22
  }
23
23
 
24
24
  std::string RangeLiteral::toString() const {
25
+ // Print back the form that was written. Emitting the synthesized step for
26
+ // a two-argument range would round-trip `[5:0]` into `[5 : 1 : 0]`, which
27
+ // reads identically but suppresses the evaluator's backwards-range warning.
28
+ if (implicitStep) return "[" + start->toString() + " : " + end->toString() + "]";
25
29
  return "[" + start->toString() + " : " + step->toString() + " : " + end->toString() + "]";
26
30
  }
27
31
 
@@ -46,11 +46,13 @@ NodePtr makeUndefinedLiteral(ParserDriver& driver, const OscadLocation& loc) {
46
46
  }
47
47
 
48
48
  NodePtr makeRangeLiteral(ParserDriver& driver, const OscadLocation& loc, NodePtr start, NodePtr end, NodePtr step) {
49
- if (!step) {
49
+ const bool implicitStep = !step;
50
+ if (implicitStep) {
50
51
  step = std::make_unique<NumberLiteral>(driver.toPosition(loc), 1.0);
51
52
  }
52
53
  return std::make_unique<RangeLiteral>(driver.toPosition(loc), nodeCast<Expression>(std::move(start)),
53
- nodeCast<Expression>(std::move(end)), nodeCast<Expression>(std::move(step)));
54
+ nodeCast<Expression>(std::move(end)), nodeCast<Expression>(std::move(step)),
55
+ implicitStep);
54
56
  }
55
57
 
56
58
  NodePtr makePositionalArgument(ParserDriver& driver, const OscadLocation& loc, NodePtr expr) {
@@ -112,6 +112,7 @@ json toJsonImpl(const ASTNode& node, bool includePos) {
112
112
  j["start"] = valueToJson(n.start.get(), includePos);
113
113
  j["end"] = valueToJson(n.end.get(), includePos);
114
114
  j["step"] = valueToJson(n.step.get(), includePos);
115
+ if (n.implicitStep) j["implicitStep"] = true;
115
116
  break;
116
117
  }
117
118
  case NodeKind::ParameterDeclaration: {
@@ -410,7 +411,8 @@ const std::unordered_map<std::string, Builder>& registry() {
410
411
  {"RangeLiteral",
411
412
  [](const json& j, Position pos) -> std::unique_ptr<ASTNode> {
412
413
  return std::make_unique<RangeLiteral>(std::move(pos), childFromJson<Expression>(j, "start"),
413
- childFromJson<Expression>(j, "end"), childFromJson<Expression>(j, "step"));
414
+ childFromJson<Expression>(j, "end"), childFromJson<Expression>(j, "step"),
415
+ j.value("implicitStep", false));
414
416
  }},
415
417
  {"ParameterDeclaration",
416
418
  [](const json& j, Position pos) -> std::unique_ptr<ASTNode> {
@@ -515,3 +515,16 @@ TEST(PrettyPrint, AssignmentRhsWithLeadingCommentAndTrailingLineComment) {
515
515
  // no other translation unit -- including this test file -- can name them.
516
516
  // Left as a known, permanent coverage ceiling rather than weakening the
517
517
  // encapsulation just to reach 100%.
518
+
519
+ // The two-argument range keeps its shape through a print/reparse cycle.
520
+ // It would be easy to always print the synthesized step, and the result
521
+ // would still be a correct program -- but `[5:0]` and `[5:1:0]` mean
522
+ // different things to the evaluator's backwards-range warning, so
523
+ // reformatting a file must not quietly convert one into the other.
524
+ TEST(PrettyPrint, ImplicitRangeStepSurvivesRoundTrip) {
525
+ auto ast = parseAst("a = [5:0];\nb = [5:1:0];\n");
526
+ const std::string printed = toOpenscad(ast);
527
+ EXPECT_NE(printed.find("[5 : 0]"), std::string::npos) << printed;
528
+ EXPECT_NE(printed.find("[5 : 1 : 0]"), std::string::npos) << printed;
529
+ expectStablePrint("a = [5:0];\nb = [5:1:0];\n");
530
+ }
@@ -426,6 +426,21 @@ enum class Op {
426
426
  // own `groupSizes.push_back(Value{...})` loop.
427
427
  CsgGroupEnd,
428
428
 
429
+ // One `children(..., separate=true)` statement inside a CSG bracket.
430
+ // `a` interns the ModularCall in nativeStatements.
431
+ //
432
+ // A statement of its own, rather than a CsgGroupStart/End pair around
433
+ // compiled code, because how many groups it contributes is only known
434
+ // at RUNTIME -- `children([2:$children-1], separate=true)` reads
435
+ // $children, and the compiler emits its group boundaries long before
436
+ // any of that exists. So this op expands the call (the same
437
+ // Evaluator::expandSeparatingChildren the interpreter uses, so the two
438
+ // engines cannot drift) and records one group per expanded statement.
439
+ //
440
+ // Not an interpreter bail: the forwarded statements go through
441
+ // evalChildren, which self-gates and retries its own compiled chunks.
442
+ CsgGroupChildren,
443
+
429
444
  // Closes the matching Op::PushCsgWrap bracket: pops
430
445
  // VmFrame::csgWrapStack's own top entry FIRST (before anything that can
431
446
  // itself throw, e.g. setTreeDepthOrThrow below -- same "exception-
@@ -34,6 +34,7 @@ struct CSGNode {
34
34
  CSGParams params; // resolve step's plain-data output
35
35
  bool uncacheable = false; // set by a later phase (ManifoldCache, Phase 8)
36
36
 
37
+
37
38
  // The call site that entered this node's call chain from the top
38
39
  // level, captured at RESOLVE time -- non-owning, AST-lifetime-bound
39
40
  // like `node`. nullptr for a node resolved at top level.
@@ -73,4 +74,5 @@ struct CSGNode {
73
74
  std::optional<std::string> cachedKey;
74
75
  };
75
76
 
77
+
76
78
  } // namespace oscadeval
@@ -190,6 +190,7 @@ public:
190
190
  // statement.
191
191
  size_t currentTreeFrameSize() const { return treeStack_.back().size(); }
192
192
 
193
+
193
194
  // Generate pass: walks `tree` bottom-up (children before their own
194
195
  // node), calling each node's registered GenerateFn (falling back to
195
196
  // concatenating children's bodies for a kind with none registered).
@@ -342,9 +343,56 @@ public:
342
343
  struct ChildrenForward {
343
344
  EvalContext evalCtx;
344
345
  std::vector<const oscad::ASTNode*> nodes;
346
+ // The `separate` flag as written at the call. Read by
347
+ // expandSeparatingChildren, which turns the forward into one
348
+ // statement per child; builtinChildren itself ignores it, since by
349
+ // the time a forward runs the expansion has already happened.
350
+ bool separate = false;
351
+ // Which of the caller's geometry statements `nodes` came from, in
352
+ // the same order. Needed by expandChildStatements, which turns each
353
+ // one back into its own `children(k)` statement.
354
+ std::vector<size_t> pickedIndices;
345
355
  };
346
356
  std::optional<ChildrenForward> prepareChildrenForward(const CallArgs& args, EvalContext& ctx);
347
357
 
358
+ // A child-statement block with every `children(..., separate=true)`
359
+ // replaced by one synthetic `children(k)` statement per child it
360
+ // selects. That is the whole of what separate=true does: the forwarded
361
+ // children become real, countable statements at this call site, so
362
+ // $children sees them, children(i) indexes them, and a boolean operator
363
+ // gives each its own operand group -- all for free, because they ARE
364
+ // statements rather than nodes wearing a flag.
365
+ //
366
+ // Called wherever a block is consumed: a module call's children
367
+ // (buildModuleChildCtx), and union/difference/intersection/
368
+ // intersection_for's own child list.
369
+ std::vector<const oscad::ASTNode*> expandChildStatements(
370
+ const std::vector<std::unique_ptr<oscad::ASTNode>>& block, EvalContext& ctx);
371
+
372
+ // One synthetic `children(index)` statement, owned by syntheticNodes_.
373
+ const oscad::ASTNode* makeChildrenIndexCall(const oscad::ModularCall& origin, size_t index);
374
+
375
+ // The sole argument of `is_undef(x)` when x is a bare identifier, else
376
+ // nullptr. That call is a PROBE -- asking whether a name is defined must
377
+ // not itself complain that it isn't -- so the name is read with
378
+ // warnIfUndef=false. Static and syntactic so the bytecode compiler can
379
+ // ask at compile time and emit LoadFreeNoWarn.
380
+ static const oscad::Identifier* undefProbeTarget(const oscad::PrimaryCall& call);
381
+
382
+ // Syntactic gate: is this statement a `children(..., separate=true)`?
383
+ // Cheap and side-effect-free, so the bytecode compiler can ask it at
384
+ // COMPILE time to decide which opcode to emit, long before the flag's
385
+ // value (or the child count it selects) is knowable.
386
+ static bool isSeparatingChildrenCall(const oscad::ASTNode& stmt);
387
+
388
+ // The runtime half: the statements `call` expands to. nullopt means the
389
+ // `separate` flag resolved falsey after all, and the caller should run
390
+ // the original statement unchanged. Shared by expandChildStatements
391
+ // (interpreter) and Op::CsgGroupChildren (VM) so the engines cannot
392
+ // drift on what expansion means.
393
+ std::optional<std::vector<const oscad::ASTNode*>> expandSeparatingChildren(const oscad::ModularCall& call,
394
+ EvalContext& ctx);
395
+
348
396
  // "WARNING: {message}{locSuffix(position)}" via echoFn_, no-op if unset.
349
397
  // Public: builtins/import.cpp's not-manifold warning is emitted from a
350
398
  // free function, same reasoning as tagGenerated()/builtinChildren().
@@ -392,7 +440,12 @@ public:
392
440
  Value applyUnaryOp(oscad::NodeKind kind, const Value& v, const oscad::Position& pos);
393
441
  Value applyIndexAccess(const Value& obj, const Value& idx);
394
442
  Value applyMemberAccess(const Value& obj, const std::string& member);
395
- Value applyRange(const Value& startV, const Value& endV, const Value& stepV);
443
+ // `implicitStep` is the RangeLiteral's own flag: true when the source
444
+ // wrote `[a:b]` and the parser supplied the 1.0. It only affects whether
445
+ // the backwards-range warning fires; the resulting value is the same
446
+ // either way. `pos` locates that warning.
447
+ Value applyRange(const Value& startV, const Value& endV, const Value& stepV, bool implicitStep = false,
448
+ const oscad::Position* pos = nullptr);
396
449
 
397
450
  // Entry point for a call site resolved INSIDE compiled bytecode (the
398
451
  // CALL_FN opcode, bytecode_vm.cpp): `bound` is already fully evaluated
@@ -1622,6 +1675,12 @@ public:
1622
1675
  // is on top *at that moment* -- the frame this node's own build call
1623
1676
  // pushed just before resolving. See csg_resolve.cpp for the full
1624
1677
  // push/call/pop sequence.
1678
+ // Synthetic `children(k)` calls built by expandChildStatements. Owned
1679
+ // here because CSGNode::node and idToNode hold non-owning pointers into
1680
+ // them, so they must outlive the pass; cleared with those maps at
1681
+ // resolveTreeImpl entry.
1682
+ std::vector<std::unique_ptr<oscad::ASTNode>> syntheticNodes_;
1683
+
1625
1684
  std::vector<std::vector<std::unique_ptr<CSGNode>>> treeStack_;
1626
1685
 
1627
1686
  private:
@@ -97,6 +97,13 @@ void writeVrml(const std::string& path, const std::vector<ExportObject>& objects
97
97
  // Color) and Shape level 1 (Appearance, Material), exactly the nodes used.
98
98
  void writeX3d(const std::string& path, const std::vector<ExportObject>& objects);
99
99
 
100
+ // AMF 1.1. XML like X3D, but its colour lives on the <volume>, not the
101
+ // face: a mesh holds one vertex list and one <volume> per material. An
102
+ // object whose triangles are not all one colour therefore becomes several
103
+ // volumes -- which is what a multimaterial slicer reads as separate
104
+ // materials to assign to tools.
105
+ void writeAmf(const std::string& path, const std::vector<ExportObject>& objects);
106
+
100
107
  // OFF (Object File Format): header "OFF", "$verts $tris 0", vertex lines,
101
108
  // then "3 i j k" per triangle (0-indexed, count-prefixed). Mirrors
102
109
  // export.py's write_off.
@@ -355,20 +355,6 @@ private:
355
355
  // it, keeping expandIterable() itself free of any Evaluator/echo coupling.
356
356
  using RangeTooManyFn = std::function<void(size_t count)>;
357
357
 
358
- // Called when a range's step points AWAY from its end -- begin > end with a
359
- // positive step, or begin < end with a negative step. Such a range is
360
- // naturally empty, and the reference warns rather than iterating zero times
361
- // in silence. `stepPositive` picks which of its two messages applies; use
362
- // rangeDirectionWarning() to build the text.
363
- //
364
- // Separate from RangeTooManyFn for the same reason that one exists: it
365
- // keeps expandIterable() free of any Evaluator/echo coupling.
366
- using RangeDirectionFn = std::function<void(bool stepPositive)>;
367
-
368
- // The reference's own wording, quoted verbatim, for a range whose step
369
- // points away from its end. Shared so the five call sites cannot drift.
370
- std::string rangeDirectionWarning(bool stepPositive);
371
-
372
358
  // Converts a for()/intersection_for() loop assignment's evaluated RHS into
373
359
  // the sequence of values to iterate: undef -> empty, range -> a LAZY
374
360
  // sequence (matching OscRange's own start/step/end iteration, half-open
@@ -394,8 +380,7 @@ std::string rangeDirectionWarning(bool stepPositive);
394
380
  // one with a live consumer (BelfrySCAD) that surfaced it. The count is
395
381
  // computed in closed form (not IterableValues::size()'s O(n) walk) so a
396
382
  // legitimate huge-but-under-the-limit range never pays an eager pass.
397
- IterableValues expandIterable(const Value& v, const RangeTooManyFn& onTooMany = nullptr,
398
- const RangeDirectionFn& onWrongDirection = nullptr);
383
+ IterableValues expandIterable(const Value& v, const RangeTooManyFn& onTooMany = nullptr);
399
384
 
400
385
  // `each <body>`'s own flatten-one-level rule, shared by the AST interpreter
401
386
  // (evalListLiteral/evalListElement's ListCompEach handling, expr_eval.cpp)
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.40.0"
7
+ version = "0.50.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -315,8 +315,14 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
315
315
 
316
316
 
317
317
  def export_extensions() -> list:
318
- """The file extensions export_model understands, dot-prefixed."""
319
- return [".3mf", ".stl", ".obj", ".off", ".ply", ".wrl", ".x3d"]
318
+ """The file extensions export_model understands, dot-prefixed.
319
+
320
+ Asked of the extension rather than restated here. The hardcoded copy
321
+ this replaces had already drifted: it was missing formats the C++
322
+ writer table had gained, and nothing failed -- callers simply could
323
+ not reach them.
324
+ """
325
+ return list(_ext.export_extensions())
320
326
 
321
327
 
322
328
  def check_mesh(verts, tris) -> dict:
@@ -504,6 +510,17 @@ class Evaluator:
504
510
  except EvalError:
505
511
  raise
506
512
  except Exception as e: # ParseError/EvalError from C++ arrive as RuntimeError
513
+ # The binding attaches whatever the script echoed before it failed
514
+ # as args[1] (see bindings/module.cpp). Replay it first: a failed
515
+ # run's own output is the most useful thing it produced, and
516
+ # dropping it means the console shows the error with none of the
517
+ # context that led to it.
518
+ args = getattr(e, "args", ())
519
+ if len(args) == 2 and isinstance(args[1], list):
520
+ if self._echo_fn:
521
+ for line in args[1]:
522
+ self._echo_fn(line)
523
+ raise EvalError(str(args[0])) from e
507
524
  raise EvalError(str(e)) from e
508
525
  bodies = bodies_from_dicts(body_dicts)
509
526
  # originalID -> a node with `.position` (start/end offsets) for WYSIWYG
@@ -46,6 +46,14 @@ add_library(openscad_cpp_evaluator STATIC
46
46
  builtins/surface.cpp
47
47
  )
48
48
 
49
+ # Seeds $_BELFRYSCAD (eval_context.cpp). Parsed from pyproject.toml by the
50
+ # top-level CMakeLists so there is one source of truth for the version.
51
+ target_compile_definitions(openscad_cpp_evaluator PRIVATE
52
+ OSCAD_EVAL_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
53
+ OSCAD_EVAL_VERSION_MINOR=${PROJECT_VERSION_MINOR}
54
+ OSCAD_EVAL_VERSION_PATCH=${PROJECT_VERSION_PATCH}
55
+ )
56
+
49
57
  target_include_directories(openscad_cpp_evaluator PUBLIC
50
58
  $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
51
59
  )
@@ -68,7 +68,13 @@ std::optional<manifold::CrossSection> toCrossSection(const std::vector<ColoredBo
68
68
  // operand (unioned together), and each subsequent statement's bodies are
69
69
  // unioned then subtracted -- a flat evaluation would lose this grouping and
70
70
  // misbehave when e.g. BOSL2's attachable() returns multiple bodies (parent
71
- // + attached children) as one operand. Mirrors _resolve_csg/_generate_csg,
71
+ // + attached children) as one operand.
72
+ //
73
+ // One statement can still turn into several: children(separate=true) is
74
+ // expanded into one `children(k)` statement per child it forwards, before
75
+ // this loop ever sees the block (Evaluator::expandChildStatements), which is
76
+ // how `difference() children(separate=true)` subtracts children 1..n from
77
+ // child 0. Nothing special happens here -- they are simply statements. Mirrors _resolve_csg/_generate_csg,
72
78
  // minus _attach_tri_colors' multi-color-merge provenance (ponytail: a
73
79
  // merged body always takes its first contributing child's color, matching
74
80
  // this evaluator's own pre-tri_colors baseline behavior -- revisit
@@ -87,11 +93,11 @@ CSGParams resolveCsg(Evaluator& ev, const oscad::ModularCall& node, EvalContext&
87
93
 
88
94
  std::vector<const oscad::ASTNode*> assignNodes;
89
95
  std::vector<const oscad::ASTNode*> geoNodes;
90
- for (const auto& c : node.children) {
96
+ for (const oscad::ASTNode* c : ev.expandChildStatements(node.children, effCtx)) {
91
97
  if (c->kind() == oscad::NodeKind::Assignment) {
92
- assignNodes.push_back(c.get());
98
+ assignNodes.push_back(c);
93
99
  } else if (c->kind() != oscad::NodeKind::ModuleDeclaration && c->kind() != oscad::NodeKind::FunctionDeclaration) {
94
- geoNodes.push_back(c.get());
100
+ geoNodes.push_back(c);
95
101
  }
96
102
  }
97
103
  if (!assignNodes.empty()) ev.evalChildren(assignNodes, effCtx);
@@ -101,8 +107,7 @@ CSGParams resolveCsg(Evaluator& ev, const oscad::ModularCall& node, EvalContext&
101
107
  for (const oscad::ASTNode* geoNode : geoNodes) {
102
108
  const size_t before = ev.currentTreeFrameSize();
103
109
  ev.evalChildren(std::vector<const oscad::ASTNode*>{geoNode}, effCtx);
104
- const size_t after = ev.currentTreeFrameSize();
105
- groupSizes.push_back(Value{static_cast<double>(after - before)});
110
+ groupSizes.push_back(Value{static_cast<double>(ev.currentTreeFrameSize() - before)});
106
111
  }
107
112
 
108
113
  CSGParams params;