openscad-cpp-evaluator 1.14.0__tar.gz → 1.18.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 (218) hide show
  1. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/CLAUDE.md +71 -1
  2. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/bindings/module.cpp +21 -12
  4. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/colored_body.hpp +43 -0
  5. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_use.hpp +12 -0
  6. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/evaluator.hpp +30 -0
  7. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/export.hpp +8 -1
  8. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/scope_trail.hpp +40 -4
  9. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/pyproject.toml +1 -1
  10. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/python/openscad_cpp_evaluator/__init__.py +13 -3
  11. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/booleans.cpp +32 -3
  12. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/color.cpp +2 -2
  13. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/control.cpp +1 -0
  14. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/text.cpp +12 -1
  15. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/bytecode_vm.cpp +1 -1
  16. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/csg_generate.cpp +22 -2
  17. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/csg_resolve.cpp +2 -0
  18. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/eval_use.cpp +6 -0
  19. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/export.cpp +16 -4
  20. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/expr_eval.cpp +35 -0
  21. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/user_calls.cpp +6 -2
  22. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_booleans.cpp +27 -0
  23. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_export.cpp +60 -0
  24. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_include_cache.cpp +2 -0
  25. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_manifold_cache.cpp +7 -8
  26. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_python_bindings.py +21 -0
  27. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_scope_trail.cpp +64 -0
  28. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_text.cpp +27 -0
  29. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_use.cpp +56 -0
  30. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/cli_lib.cpp +1 -0
  31. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.github/workflows/ci.yml +0 -0
  32. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.github/workflows/wheels.yml +0 -0
  33. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.gitignore +0 -0
  34. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.gitmodules +0 -0
  35. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/CMakeLists.txt +0 -0
  36. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/LICENSE +0 -0
  37. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/README.md +0 -0
  38. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/bindings/CMakeLists.txt +0 -0
  39. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/bindings/ast_to_py.cpp +0 -0
  40. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/cmake/embed_font.cmake +0 -0
  41. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/CMakeLists.txt +0 -0
  42. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/eval_perf_benchmark.cpp +0 -0
  43. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/manifold_cache_reuse.cpp +0 -0
  44. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/minimal_debugger.cpp +0 -0
  45. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  46. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/.gitignore +0 -0
  47. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  48. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  49. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/README.md +0 -0
  50. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  51. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  52. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  53. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  54. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  55. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  56. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  57. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  58. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  59. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  60. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  61. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  62. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  63. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  64. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  65. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  66. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  67. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  68. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  69. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  70. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  71. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  72. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  73. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  74. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  75. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  76. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  77. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  78. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  79. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  80. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  81. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  82. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  83. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  84. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  85. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  86. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  87. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  88. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  89. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  90. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  91. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  92. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  93. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  94. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  95. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  96. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  97. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  98. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  99. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  100. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  101. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  102. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  103. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  104. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  105. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  106. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  107. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  108. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  109. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  110. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  111. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  112. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  113. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  114. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  115. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  116. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  117. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  118. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  119. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  120. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  121. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  122. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  123. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  124. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  125. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  126. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  127. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  128. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  129. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  130. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  131. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  132. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  133. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  134. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  135. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  136. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  137. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  138. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  139. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  140. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  141. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  142. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  143. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  144. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LICENSE +0 -0
  145. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  146. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  147. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  148. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  149. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/CMakeLists.txt +0 -0
  150. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/builtins.hpp +0 -0
  151. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/call_args.cpp +0 -0
  152. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/dxf_dim.cpp +0 -0
  153. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/extrude.cpp +0 -0
  154. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/function_builtins.cpp +0 -0
  155. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/import.cpp +0 -0
  156. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/modifiers.cpp +0 -0
  157. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/primitives_2d.cpp +0 -0
  158. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/primitives_3d.cpp +0 -0
  159. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/registry.cpp +0 -0
  160. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/roof.cpp +0 -0
  161. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/segments.cpp +0 -0
  162. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/surface.cpp +0 -0
  163. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/topology.cpp +0 -0
  164. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/transforms.cpp +0 -0
  165. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/bytecode_compiler.cpp +0 -0
  166. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/colored_body.cpp +0 -0
  167. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/css_colors.cpp +0 -0
  168. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/debug/debug_repl.cpp +0 -0
  169. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/debug_profile.cpp +0 -0
  170. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/eval_context.cpp +0 -0
  171. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/eval_error.cpp +0 -0
  172. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/format_closure.cpp +0 -0
  173. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/dxf_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/mesh_import.cpp +0 -0
  175. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/surface_load.cpp +0 -0
  176. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/svg_import.cpp +0 -0
  177. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/manifold_cache.cpp +0 -0
  178. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/measure_geometry.cpp +0 -0
  179. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/mesh_check.cpp +0 -0
  180. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/stmt_eval.cpp +0 -0
  181. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/text/font_match.cpp +0 -0
  182. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/text/freetype_font_provider.cpp +0 -0
  183. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/text/text_metrics.cpp +0 -0
  184. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/utf8.cpp +0 -0
  185. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/value.cpp +0 -0
  186. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/zip_stored.cpp +0 -0
  187. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/CMakeLists.txt +0 -0
  188. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_bytecode_compiler.cpp +0 -0
  189. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_children_separate.cpp +0 -0
  190. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_cli.cpp +0 -0
  191. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_control_flow.cpp +0 -0
  192. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_csg_tree.cpp +0 -0
  193. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_debug_hooks.cpp +0 -0
  194. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_dxf_svg_import.cpp +0 -0
  195. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_eval_context.cpp +0 -0
  196. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_expr_eval.cpp +0 -0
  197. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_extrude_roof.cpp +0 -0
  198. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_feature_detection.cpp +0 -0
  199. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_function_builtins.cpp +0 -0
  200. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_helpers.hpp +0 -0
  201. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_import_export.cpp +0 -0
  202. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_mesh_check.cpp +0 -0
  203. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_multi_color_merge.cpp +0 -0
  204. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_primitives.cpp +0 -0
  205. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_profiling.cpp +0 -0
  206. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_render_expr.cpp +0 -0
  207. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_scoping.cpp +0 -0
  208. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_surface.cpp +0 -0
  209. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_tail_calls.cpp +0 -0
  210. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_transforms.cpp +0 -0
  211. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_unexpected_args.cpp +0 -0
  212. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_value.cpp +0 -0
  213. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_viewport_params.cpp +0 -0
  214. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_zip_stored.cpp +0 -0
  215. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/CMakeLists.txt +0 -0
  216. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/cli_lib.hpp +0 -0
  217. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/main.cpp +0 -0
  218. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/uv.lock +0 -0
@@ -298,6 +298,13 @@ variable real OpenSCAD reads). Family match is case-insensitive and required; st
298
298
  dropping to Regular beats falling through to an unrelated family, which is what fontconfig does
299
299
  too.
300
300
 
301
+ **`text()` takes `font` positionally too — and NOTHING after it.** `text(text, size, font)` is the
302
+ positional set; `halign`/`valign`/`spacing`/`direction`/`language`/`script` are name-only, because
303
+ that is what the 2026.02.01 binary does: `text("Hi", 10, "F", "center")` is not centred there while
304
+ `halign="center"` is, measured by bounding box. So **`text()` and `textmetrics()` genuinely differ**
305
+ — the latter takes all nine positionally — and neither can be assumed from the other. `font` being
306
+ name-only made `text("Hi", 10, "Liberation Sans:style=Bold")` silently draw the default face.
307
+
301
308
  **`font` is a POSITIONAL argument in both metrics functions**, and so is everything after it:
302
309
  `fontmetrics(size, font)` and `textmetrics(text, size, font, direction, language, script, halign,
303
310
  valign, spacing)`. Verified against the 2026.02.01 binary rather than read off a doc page. Having
@@ -442,6 +449,39 @@ closed in one pass after a fresh "anything still unimplemented?" audit. Each bel
442
449
  all (`resolve_use_scopes`/`UseStatement` had zero hits in `openscad_evaluator`'s own `tests/`), so
443
450
  these were verified against real reference *behavior*, not ported from an existing test.
444
451
 
452
+ **File-level globals are evaluated ONCE per run (v1.16.0).** The main file's and every
453
+ `include`'s are, by the top-level statement pass, into the root `let_` level; a `use`d file's
454
+ are not -- `use` injects only declarations -- so `Evaluator::fileGlobal` evaluates ALL of that
455
+ file's top-level assignments, in source order, into a context of its own the first time anything
456
+ in the file reads one, and keeps it for the run. `ResolvedUseScopes::usedFileGlobals` carries
457
+ the per-file assignment lists and every entry point hands them over with
458
+ `Evaluator::setUsedFileGlobals` (CLI, both binding paths, the tests); a caller that skips that
459
+ keeps the old per-read fallback. Before this, `evalIdentifier`'s scope fallback re-evaluated the
460
+ assignment's expression on EVERY read from inside a function -- `callCtx()` isolates `let_` so the
461
+ root level was invisible -- which made `r = rands(0,1,1); function g() = r;` give `g() != g()`
462
+ (OpenSCAD: equal) and a 20K-element global list read 2000 times from a function cost 9.7s
463
+ (now 0.02s). Two deliberate divergences from OpenSCAD 2026.02.01, both verified against it:
464
+ a used file's globals are evaluated once, not once per call into the file (upstream's per-call
465
+ re-evaluation is a long-standing bug there, so its forward-read warning prints once here, not
466
+ per call); and a global read from a function BEFORE its assignment has run is `undef` with an
467
+ "Ignoring unknown variable" warning, exactly as upstream, where the old fallback had quietly
468
+ computed the not-yet-assigned value.
469
+ **Scope-trail level table shrinks (v1.18.0).** `ScopeTrailStorage::parent_` and its indexed twin
470
+ map level -> parent level in a vector indexed by level. Levels came from `++nextLevel_` and were
471
+ never reused, so the vector grew 4 bytes per level for the whole run -- 3 x 64MB on a 13M-call
472
+ script, plus the copy at every doubling -- and was 192MB of fractal_tree.scad's peak. A popped
473
+ level is now marked -1 (0 still means "live, no parent": an isolated root), and when the NEWEST
474
+ level dies `markDead` pops the vector back past every dead level above the newest live one, whose
475
+ numbers `openLevel` then hands out again. Every invariant `lookup()`'s sorted-merge walk relies on
476
+ holds: a reused number is greater than every level still holding an entry (all higher levels were
477
+ dead and `popLevel` had already removed their entries), so a name's push-stack stays ascending,
478
+ and a live level's number never changes -- an escaping closure that keeps a level alive simply
479
+ stops the shrink at it (`tests/test_scope_trail.cpp` covers both). The ancestry walk now stops at
480
+ `ancestor > 0` rather than `!= 0`. Same PR: `callCtxFor`'s per-call scan over active declarations
481
+ does the integer span test before the origin STRING compare (`_platform_memcmp` was 1.4% of
482
+ fractal_tree), and driveVm's completion branch no longer copies the finished frame's name. Anklet
483
+ 74 -> 63MB, a 4M-call synthetic 380 -> 258MB, fractal_tree -3.4% wall.
484
+
445
485
  **Known gap closed in Phase 3, worth remembering for Phase 5+**: every builtin resolve function must
446
486
  call `resolveCallArgs` (call_args.hpp), never bare `resolveArgs`, or a `$`-prefixed named call
447
487
  argument (`circle(r=2, $fn=64)`) silently fails to reach `ctx.dyn`. Caught by a geometric-correctness
@@ -903,6 +943,28 @@ grep for `ponytail:`.
903
943
  operation`. `linear_extrude`/`rotate_extrude`/`offset` are always-2D and `projection` always-3D, so
904
944
  they skip the "mixing" line and emit only the second; `roof()` emits neither, matching the
905
945
  reference. Background (`%`) children are exempt.
946
+ **The emptiness test is cached on the body (`ColoredBody::knownEmpty`, v1.17.0).** It used to
947
+ call `Manifold::NumTri()` on every child at every level, and every Manifold accessor goes through
948
+ `GetImpl()`, which materialises a lazy transform (a full mesh + collider copy) and runs a lazy
949
+ boolean -- so each leaf was copied once per ancestor transform, and because every node kept its
950
+ `bodies` until the tree died, all those copies stayed alive: fractal_tree.scad held 72,929
951
+ transformed copies of 1024 leaves (3.1GB). Now a body is asked once, the flag rides along on the
952
+ struct copies transform/colour/flatten make (emptiness is transform-invariant, and every generate
953
+ function that builds a new body starts from a fresh `ColoredBody`), and `generateTreeImpl` clears
954
+ each child's `bodies` once the parent has consumed them (move-assigning a fresh vector: `clear()`
955
+ and `= {}` both keep the capacity, and 35K nodes x 592-byte slots was 230MB) -- only top-level
956
+ bodies leave, a cache hit re-fills from the cache, and the bindings' `csg_tree` facade no longer
957
+ converts per-node bodies (it used to `GetMeshGL()` every node into numpy on every evaluate(), for
958
+ a dump that never read them). Three more forcing points had to go for the chain to stay lazy: the
959
+ booleans' `Status()` operand filter (now `bodyStatus()`, same cache), the boolean RESULT's own
960
+ emptiness (derived from the operands' flags -- a union is non-empty if any operand is -- rather
961
+ than asked), and `attachTriColors`, which `GetMeshGL()`'d every boolean result and now runs only
962
+ when the operands could actually disagree on colour. `Evaluator::generatedNodeCount` replaced
963
+ "does the child have bodies" as the cache-hit proxy in tests. Transforms compose lazily inside
964
+ Manifold and each leaf is transformed once, at the boolean that needs it. fractal_tree 3.1GB ->
965
+ 0.67GB, Dalek 370 -> 277MB, snappy-reprap full_assembly 1.6GB -> 0.87GB; wall time neutral to
966
+ -3% interleaved. The output is geometrically identical (triangle count, area, volume) but not
967
+ byte-identical: Manifold evaluates one flat batch union instead of our old level-by-level order.
906
968
 
907
969
  Doing this centrally is also what keeps `generateCsg` safe: it builds ONE result and switches on
908
970
  whether it holds a `body` or a `section`, so a group that changed dimension part-way dereferenced
@@ -970,7 +1032,15 @@ grep for `ponytail:`.
970
1032
  restating the list, because three hand-written copies had already drifted far enough that `.off`
971
1033
  could be written by neither the GUI nor the CLI while every test passed.
972
1034
  Colour is carried by 3MF, AMF, OBJ (companion `.mtl`), PLY, VRML and X3D; STL and OFF compose
973
- every body into one solid and store geometry only. **AMF** puts colour on the `<volume>` rather
1035
+ every body into one solid and store geometry only.
1036
+ **`ExportOptions::splitColors` picks what the file is FOR**, and is the axis worth asking a user
1037
+ about rather than "should parts be separate": on (the default) writes one object per colour, which
1038
+ is what a **multi-material** print wants — each colour is something the slicer assigns to a
1039
+ filament. Off is a **single-material** print: everything unions into one welded solid, so touching
1040
+ colour regions lose the coincident faces that stacking per-colour objects would leave, and the
1041
+ per-triangle colour is dropped because such a print has no use for it. Implemented by forcing
1042
+ `splitBodiesForExport`'s `allSame` branch, which already unions rather than concatenates —
1043
+ concatenating the per-colour objects instead would have left the seam. **AMF** puts colour on the `<volume>` rather
974
1044
  than the face, so an object whose triangles are not all one colour is written as one volume per
975
1045
  colour — per-triangle `<color>` is legal in the spec but poorly supported by the slicers that are
976
1046
  the audience for it.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.14.0
3
+ Version: 1.18.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -223,8 +223,9 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
223
223
  return d;
224
224
  }
225
225
 
226
- // CSGNode -> a facade `_CSGNode` (kind/params/bodies/is_builtin/children),
227
- // recursively, eagerly converting every field to plain Python data. No
226
+ // CSGNode -> a facade `_CSGNode` (kind/params/is_builtin/children; `bodies`
227
+ // is always empty, see below), recursively, eagerly converting every field
228
+ // to plain Python data. No
228
229
  // lifetime extension needed -- CSGParams (Value-only, see csg_node.hpp) and
229
230
  // ColoredBody never hold AST pointers, so the result is fully self-contained
230
231
  // once built, unlike CSGNode::node itself (deliberately not exposed: the AST
@@ -234,15 +235,14 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
234
235
  nb::object csgNodeToPy(const oscadeval::CSGNode& node) {
235
236
  nb::dict params;
236
237
  for (const auto& [k, v] : node.params) params[nb::str(k.c_str())] = valueToPy(v);
238
+ // Deliberately no bodies. This used to GetMeshGL() every body of every
239
+ // node in the tree into numpy on every evaluate() -- and the only
240
+ // consumer, format_csg_tree(), never read them. Since v1.17.0 an inner
241
+ // node's bodies are released once its parent has consumed them (see
242
+ // generateTreeImpl), so there is nothing here to convert anyway; and a
243
+ // body the tree still holds may be a LAZY Manifold whose GetMeshGL()
244
+ // would materialise the very transform that release exists to avoid.
237
245
  nb::list bodies;
238
- for (const oscadeval::ColoredBody& cb : node.bodies) {
239
- if (!cb.isDisplayOnly() && (!cb.body || cb.body->IsEmpty())) continue;
240
- // ColoredBody here is const in this walk (tree ownership stays with
241
- // the caller) -- GetMeshGL() itself is logically read-only, so a
242
- // const_cast here is safe/local, mirroring bodyToDict's mutating
243
- // signature only because manifold3d's own API isn't const-qualified.
244
- bodies.append(bodyToDict(const_cast<oscadeval::ColoredBody&>(cb)));
245
- }
246
246
  nb::list children;
247
247
  for (const std::unique_ptr<oscadeval::CSGNode>& child : node.children) children.append(csgNodeToPy(*child));
248
248
  return facadeAttr("_CSGNode")(node.kind, params, bodies, node.isBuiltin, children);
@@ -404,7 +404,8 @@ void applyPdfOptions(const nb::dict& d, oscadeval::ExportPdfOptions& pdf) {
404
404
  }
405
405
 
406
406
  nb::list exportModelPy(const std::string& path, const Geometry& geom, const std::string& format, bool asciiStl,
407
- bool stripSlivers, bool splitComponents, bool svgFill, const std::string& svgFillColor,
407
+ bool stripSlivers, bool splitComponents, bool splitColors, bool svgFill,
408
+ const std::string& svgFillColor,
408
409
  bool svgStroke, const std::string& svgStrokeColor, double svgStrokeWidth,
409
410
  nb::object pdfOptions) {
410
411
  oscadeval::ExportPdfOptions pdf;
@@ -418,6 +419,7 @@ nb::list exportModelPy(const std::string& path, const Geometry& geom, const std:
418
419
  opts.asciiStl = asciiStl;
419
420
  opts.stripSlivers = stripSlivers;
420
421
  opts.splitComponents = splitComponents;
422
+ opts.splitColors = splitColors;
421
423
  opts.svg.fill = svgFill;
422
424
  opts.svg.fillColor = svgFillColor;
423
425
  opts.svg.stroke = svgStroke;
@@ -455,6 +457,7 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
455
457
  oscad::ParsedProgram program = oscad::getProgramFromFile(path);
456
458
  oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, logFn);
457
459
  oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile);
460
+ ev.setUsedFileGlobals(used.usedFileGlobals);
458
461
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
459
462
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp, generate));
460
463
  collectIdSpans(ev, idSpans);
@@ -762,6 +765,7 @@ nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::c
762
765
  oscadeval::Evaluator ev(echoCpp, nullptr, manifoldCache, hooks, false);
763
766
  evPtr = &ev;
764
767
  if (fastContinueSignal) ev.setFastContinueInterruptFlag(fastContinueSignal->flag());
768
+ ev.setUsedFileGlobals(used.usedFileGlobals);
765
769
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
766
770
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp));
767
771
  collectIdSpans(ev, idSpans);
@@ -850,7 +854,8 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
850
854
 
851
855
  m.def("export_model", &exportModelPy, nb::arg("path"), nb::arg("geometry"), nb::arg("format") = std::string(),
852
856
  nb::arg("ascii_stl") = false, nb::arg("strip_slivers") = true,
853
- nb::arg("split_components") = false, nb::arg("svg_fill") = false,
857
+ nb::arg("split_components") = false, nb::arg("split_colors") = true,
858
+ nb::arg("svg_fill") = false,
854
859
  nb::arg("svg_fill_color") = std::string("white"), nb::arg("svg_stroke") = true,
855
860
  nb::arg("svg_stroke_color") = std::string("black"), nb::arg("svg_stroke_width") = 0.35,
856
861
  nb::arg("pdf_options") = nb::none(),
@@ -861,6 +866,10 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
861
866
  "`split_components` gives every disconnected piece its own object in the multi-object formats "
862
867
  "(3MF, AMF, OBJ, PLY, VRML, X3D). Off by default, matching OpenSCAD, which writes one object "
863
868
  "per colour however many pieces it is in.\n\n"
869
+ "`split_colors` (on by default) writes one object per colour -- what a MULTI-MATERIAL print "
870
+ "wants, each colour being something the slicer assigns to a filament. Turn it off for a "
871
+ "single-material print: everything welds into one solid, with no seams between colour "
872
+ "regions and no per-triangle colour it has no use for.\n\n"
864
873
  "The svg_* arguments are OpenSCAD's -O export-svg/... set and apply to .svg only. "
865
874
  "svg_stroke/svg_stroke_width also pad the page, so they are not purely cosmetic. An .svg "
866
875
  "export needs an all-2D model and raises otherwise, as OpenSCAD does.\n\n"
@@ -53,6 +53,22 @@ struct ColoredBody {
53
53
  BodyRole role = BodyRole::Normal;
54
54
  std::optional<std::vector<std::array<float, 4>>> triColors; // per-triangle RGBA, multi-color CSG merges only
55
55
 
56
+ // Whether `body` is empty, once anything has asked (isEmptyBody,
57
+ // csg_generate.cpp). Asking Manifold is not free: every accessor goes
58
+ // through GetImpl(), which MATERIALIZES a lazy transform (a full copy of
59
+ // the mesh and its collider) and, for a boolean, runs it. Emptiness is
60
+ // invariant under transforms and colour, and every generate function
61
+ // that produces a genuinely new body starts from a fresh ColoredBody,
62
+ // so a copy carrying this flag is always still right. Before this, the
63
+ // dimension check at every tree level materialised every child at
64
+ // every level: fractal_tree.scad held 72,929 transformed copies of its
65
+ // 1024 leaf meshes at once (3.1GB).
66
+ std::optional<bool> knownEmpty;
67
+ // Same idea for Manifold::Status(), which the booleans consult to keep an
68
+ // invalid operand from poisoning a union: validity is transform-invariant
69
+ // too, and a boolean of valid operands is valid.
70
+ std::optional<manifold::Manifold::Error> knownStatus;
71
+
56
72
  // Set ONLY when a mesh could not be built into a valid Manifold -- in
57
73
  // practice a polyhedron() whose faces don't close the surface (it has
58
74
  // boundary edges), which Manifold reports by returning an *empty* body
@@ -75,6 +91,33 @@ struct ColoredBody {
75
91
  bool isDisplayOnly() const { return rawMesh.has_value(); }
76
92
  };
77
93
 
94
+ // The cached questions -- see ColoredBody::knownEmpty. Ask Manifold once per
95
+ // body, never once per tree level.
96
+ inline bool bodyIsEmpty(ColoredBody& b) {
97
+ if (!b.knownEmpty) b.knownEmpty = !b.body || b.body->IsEmpty();
98
+ return *b.knownEmpty;
99
+ }
100
+ inline manifold::Manifold::Error bodyStatus(ColoredBody& b) {
101
+ if (!b.knownStatus) b.knownStatus = b.body ? b.body->Status() : manifold::Manifold::Error::NoError;
102
+ return *b.knownStatus;
103
+ }
104
+
105
+ // Emptiness of a union, from its operands' own flags and nothing else:
106
+ // non-empty if any operand is known non-empty, empty if every one is known
107
+ // empty, otherwise unknown (asked of Manifold later, if anyone ever asks).
108
+ // `get` maps a range element to its ColoredBody.
109
+ template <typename Range, typename Get>
110
+ std::optional<bool> unionEmptyOf(const Range& ops, Get get) {
111
+ bool allKnownEmpty = true;
112
+ for (const auto& o : ops) {
113
+ const ColoredBody& b = get(o);
114
+ if (b.knownEmpty && !*b.knownEmpty) return false;
115
+ if (!b.knownEmpty) allKnownEmpty = false;
116
+ }
117
+ if (allKnownEmpty) return true;
118
+ return std::nullopt;
119
+ }
120
+
78
121
  // `EvalContext::color`/CSGParams round-trip: a resolve function reads
79
122
  // ctx.color (double-precision, matching Value's own numeric type) but a
80
123
  // generate function only sees CSGParams (Value-typed, no ctx access) -- so
@@ -15,9 +15,21 @@ namespace oscadeval {
15
15
  // must keep this struct alive for as long as evaluation runs against
16
16
  // .rootScope/.processedNodes, since declarations injected from a used file
17
17
  // are raw (non-owning) pointers into it.
18
+ // One used file's own top-level assignments, in source order, keyed by
19
+ // that file's root Scope (the one its injected declarations were
20
+ // re-anchored onto). Evaluator::setUsedFileGlobals takes the list so a
21
+ // used file's globals can be evaluated ONCE per run, eagerly, the first
22
+ // time anything in that file reads one -- see Evaluator::fileGlobal.
23
+ // Raw pointers into usedFileAsts, same lifetime contract as processedNodes.
24
+ struct UsedFileGlobals {
25
+ const oscad::Scope* root = nullptr;
26
+ std::vector<const oscad::ASTNode*> assignments;
27
+ };
28
+
18
29
  struct ResolvedUseScopes {
19
30
  std::vector<std::vector<std::unique_ptr<oscad::ASTNode>>> usedFileAsts;
20
31
  std::vector<std::unique_ptr<oscad::Scope>> usedFileScopes;
32
+ std::vector<UsedFileGlobals> usedFileGlobals;
21
33
  // What currentFile should actually be evaluated as: declarations
22
34
  // injected via `use` (raw pointers into usedFileAsts, above), followed
23
35
  // by currentFile's own nodes (raw pointers into the caller-owned
@@ -7,6 +7,7 @@
7
7
  #include "openscad_cpp_evaluator/csg_node.hpp"
8
8
  #include "openscad_cpp_evaluator/debug_hooks.hpp"
9
9
  #include "openscad_cpp_evaluator/eval_context.hpp"
10
+ #include "openscad_cpp_evaluator/eval_use.hpp"
10
11
  #include "openscad_cpp_evaluator/eval_error.hpp"
11
12
  #include "openscad_cpp_evaluator/font_provider.hpp"
12
13
  #include "openscad_cpp_evaluator/manifold_cache.hpp"
@@ -285,6 +286,11 @@ public:
285
286
  const oscad::ASTNode* producer);
286
287
 
287
288
  std::unordered_map<uint32_t, const oscad::ASTNode*> idToNode;
289
+ // Nodes whose bodies were actually generated this run (not served from
290
+ // the ManifoldCache). Reset per run; a diagnostic, and the only honest
291
+ // way for a test to tell a cache hit from a miss now that an inner
292
+ // node's bodies are released the moment its parent has consumed them.
293
+ std::size_t generatedNodeCount = 0;
288
294
  std::unordered_map<uint32_t, std::optional<std::array<float, 4>>> idToColor;
289
295
 
290
296
  // Called by primitive-construction generate functions (cube, sphere,
@@ -1396,6 +1402,11 @@ public:
1396
1402
  // the same decision evalUserFunction*/evalFunctionLiteral* already
1397
1403
  // make before calling lookupOrCompileChunk/lookupCompiledLiteralChunk
1398
1404
  // themselves.
1405
+ // Every used file's own top-level assignments (ResolvedUseScopes::
1406
+ // usedFileGlobals), so fileGlobal() can evaluate a file's globals once.
1407
+ // A caller that skips this keeps the old per-read fallback for used files.
1408
+ void setUsedFileGlobals(std::vector<UsedFileGlobals> globals) { usedFileGlobals_ = std::move(globals); }
1409
+
1399
1410
  bool useBytecodeVm() const {
1400
1411
  return bytecodeVmEnabled() && (!debugHooks_.debugHook || fastContinueBreakpoints_.has_value());
1401
1412
  }
@@ -1941,6 +1952,25 @@ private:
1941
1952
  // lifetime contract as every other raw AST/EvalContext pointer this
1942
1953
  // class holds).
1943
1954
  const EvalContext* rootCtx_ = nullptr;
1955
+
1956
+ // File-level globals are evaluated ONCE per run. The main file's (and
1957
+ // every include's) already are, by the top-level statement pass, into
1958
+ // rootCtx_'s own let_ level. A used file's are not -- `use` injects only
1959
+ // its declarations -- so the first read of any global from that file
1960
+ // evaluates ALL of its assignments, in source order, into a context of
1961
+ // its own, kept here for the rest of the run. Before this, a function
1962
+ // reading a global re-evaluated the assignment's expression on every
1963
+ // read (evalIdentifier's scope fallback): `r = rands(0,1,1); function
1964
+ // g() = r;` gave `g() != g()`, and a 20K-element global list read 2000
1965
+ // times from a function cost 9.7s. OpenSCAD itself re-evaluates a used
1966
+ // file's globals per CALL, which is a long-standing upstream bug, not
1967
+ // behaviour to match. Returns nullopt for a file this evaluator was
1968
+ // never told about (setUsedFileGlobals), so such a caller keeps the
1969
+ // old fallback; nullptr for a known file whose global is not (yet)
1970
+ // assigned -- a forward read, undef in OpenSCAD too.
1971
+ std::optional<const Value*> fileGlobal(const oscad::Scope& fileRoot, const std::string& name);
1972
+ std::vector<UsedFileGlobals> usedFileGlobals_;
1973
+ std::unordered_map<const oscad::Scope*, EvalContext> usedFileCtx_;
1944
1974
  // The most recent ctx seen by evalStatement() -- error()'s only way to
1945
1975
  // reach a locals snapshot for its own errorBreak hook call, since
1946
1976
  // error() itself (called from dozens of builtin resolve functions)
@@ -53,7 +53,8 @@ struct ExportObject {
53
53
  // 1-based index is appended to `openParts` for the caller to warn about.
54
54
  std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& bodies,
55
55
  std::vector<int>* openParts = nullptr,
56
- bool splitComponents = false);
56
+ bool splitComponents = false,
57
+ bool splitColors = true);
57
58
 
58
59
  // Writes a binary STL: composes every body's mesh into one solid (bodies
59
60
  // with no `.body` -- 2D-only sections -- or an empty Manifold are
@@ -236,6 +237,12 @@ struct ExportOptions {
236
237
  // pieces it is in; see that function's own comment for why the default
237
238
  // moved here.
238
239
  bool splitComponents = false;
240
+ // Rule 2 of splitBodiesForExport: one object per colour. That is what
241
+ // a MULTI-MATERIAL print wants -- each colour becomes something the
242
+ // slicer assigns to a filament. Turn it off for a single-material
243
+ // print, which wants one welded solid instead: no seams between
244
+ // colour regions, and no per-triangle colour it has no use for.
245
+ bool splitColors = true;
239
246
  // .svg only.
240
247
  ExportSvgOptions svg;
241
248
  // .pdf only.
@@ -186,7 +186,7 @@ public:
186
186
  }
187
187
  dirty_.erase(it);
188
188
  }
189
- parent_[static_cast<size_t>(level)] = 0; // dead: an ancestry walk stops here
189
+ markDead(level);
190
190
  }
191
191
 
192
192
  // nullptr if `name` has no binding on `myLevel`'s own ancestry chain.
@@ -211,7 +211,7 @@ public:
211
211
  const std::vector<Entry>& vec = it->second;
212
212
  auto idx = static_cast<long>(vec.size()) - 1;
213
213
  int ancestor = myLevel;
214
- while (ancestor != 0 && idx >= 0) {
214
+ while (ancestor > 0 && idx >= 0) {
215
215
  const int entryLevel = vec[static_cast<size_t>(idx)].level;
216
216
  if (entryLevel > ancestor) {
217
217
  --idx;
@@ -236,6 +236,11 @@ public:
236
236
  auto it = stacks_.find(name);
237
237
  return it == stacks_.end() ? 0 : it->second.size();
238
238
  }
239
+ // Test-only: how many level slots `parent_` currently holds (index 0
240
+ // included) -- lets ScopeTrailStorageTest assert that popping levels in
241
+ // call/return order really does shrink it back, and that an escaping
242
+ // closure's still-live level stops the shrink at itself.
243
+ size_t debugLevelSlotsForTesting() const { return parent_.size(); }
239
244
 
240
245
  // Ancestry-filtered snapshot -- O(total distinct names ever bound so
241
246
  // far in the whole evaluation), not O(names visible now). Only used
@@ -272,6 +277,27 @@ private:
272
277
  std::vector<int> parent_{0};
273
278
  int nextLevel_ = 0;
274
279
 
280
+ // A popped level is marked -1 (0 is a live level with no parent -- an
281
+ // isolated root -- so the two must stay distinguishable). Then, whenever
282
+ // the level being popped is the newest one, `parent_` shrinks back past
283
+ // every dead level above the newest still-live one, and its number is
284
+ // handed out again by the next openLevel(). This keeps every invariant
285
+ // lookup() relies on: a reused number is greater than every level still
286
+ // holding an entry (all higher levels were dead, and popLevel already
287
+ // removed their entries), so a name's push-stack stays ascending, and a
288
+ // live level's number never changes -- an escaping closure that keeps a
289
+ // level alive simply stops the shrink at it. Without this, `parent_`
290
+ // grew by one int per level for the whole run and never shrank: 3 x
291
+ // 64MB for a 13M-call script, plus the copy of each doubling.
292
+ void markDead(int level) {
293
+ parent_[static_cast<size_t>(level)] = -1;
294
+ if (level == nextLevel_) {
295
+ while (nextLevel_ > 0 && parent_[static_cast<size_t>(nextLevel_)] < 0) {
296
+ parent_.pop_back();
297
+ --nextLevel_;
298
+ }
299
+ }
300
+ }
275
301
  };
276
302
 
277
303
  // Per-EvalContext handle onto a shared ScopeTrailStorage<T>: this view's
@@ -473,7 +499,7 @@ public:
473
499
  }
474
500
  dirty_.erase(it);
475
501
  }
476
- parent_[static_cast<size_t>(level)] = 0; // dead: an ancestry walk stops here
502
+ markDead(level);
477
503
  }
478
504
 
479
505
  // Same sorted-merge ancestry walk as ScopeTrailStorage::lookup -- see
@@ -485,7 +511,7 @@ public:
485
511
  const std::vector<Entry>& vec = stacks_[static_cast<size_t>(id)];
486
512
  auto idx = static_cast<long>(vec.size()) - 1;
487
513
  int ancestor = myLevel;
488
- while (ancestor != 0 && idx >= 0) {
514
+ while (ancestor > 0 && idx >= 0) {
489
515
  const int entryLevel = vec[static_cast<size_t>(idx)].level;
490
516
  if (entryLevel > ancestor) {
491
517
  --idx;
@@ -505,6 +531,7 @@ public:
505
531
  int id = intern_->idFor(name);
506
532
  return id >= 0 && static_cast<size_t>(id) < stacks_.size() ? stacks_[static_cast<size_t>(id)].size() : 0;
507
533
  }
534
+ size_t debugLevelSlotsForTesting() const { return parent_.size(); } // see ScopeTrailStorage's
508
535
 
509
536
  std::vector<std::pair<std::string, T>> items(int myLevel) const {
510
537
  std::vector<std::pair<std::string, T>> result;
@@ -530,6 +557,15 @@ private:
530
557
  std::vector<int> parent_{0}; // see ScopeTrailStorage::parent_
531
558
  int nextLevel_ = 0;
532
559
 
560
+ void markDead(int level) { // see ScopeTrailStorage::markDead
561
+ parent_[static_cast<size_t>(level)] = -1;
562
+ if (level == nextLevel_) {
563
+ while (nextLevel_ > 0 && parent_[static_cast<size_t>(nextLevel_)] < 0) {
564
+ parent_.pop_back();
565
+ --nextLevel_;
566
+ }
567
+ }
568
+ }
533
569
  };
534
570
 
535
571
  // Per-EvalContext handle onto a shared IndexedScopeTrailStorage<T> -- the
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.14.0"
7
+ version = "1.18.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -123,8 +123,11 @@ class RootScope:
123
123
 
124
124
  class _CSGNode:
125
125
  """One resolved (and, after evaluate(), generated) node in the CSG tree --
126
- mirrors the reference's CSGNode dataclass. `bodies` is a list of already-
127
- converted ColoredBody dicts (see bodyToDict on the C++ side); `params` is
126
+ mirrors the reference's CSGNode dataclass. `bodies` is always empty since
127
+ v1.17.0: an inner node's bodies are released as soon as its parent has
128
+ consumed them, and converting every node's mesh to numpy on every
129
+ evaluate() was work nothing read (format_csg_tree() prints structure,
130
+ not geometry; the rendered bodies come back separately). `params` is
128
131
  plain Python data (numbers/strings/bools/lists/dicts/OscObject), never an
129
132
  AST pointer -- eagerly converted, no lifetime tie to the Evaluator that
130
133
  produced it. Used by format_csg_tree(); not otherwise part of the public
@@ -293,6 +296,7 @@ def strip_slivers(verts, tris):
293
296
 
294
297
  def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
295
298
  strip_slivers: bool = True, split_components: bool = False,
299
+ split_colors: bool = True,
296
300
  svg_fill: bool = False, svg_fill_color: str = "white",
297
301
  svg_stroke: bool = True, svg_stroke_color: str = "black",
298
302
  svg_stroke_width: float = 0.35, pdf_options: dict | None = None) -> list:
@@ -316,6 +320,12 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
316
320
  `split_components` gives every disconnected piece its own object in the
317
321
  multi-object formats. Off by default, which is what OpenSCAD writes.
318
322
 
323
+ `split_colors` (on by default) writes one object per colour -- what a
324
+ MULTI-MATERIAL print wants, each colour being something the slicer
325
+ assigns to a filament. Turn it off for a single-material print:
326
+ everything welds into one solid, with no seams between colour regions
327
+ and no per-triangle colour it has no use for.
328
+
319
329
  The `svg_*` arguments are OpenSCAD's `-O export-svg/...` set and apply
320
330
  to `.svg` only, which writes a 2D model at 1:1 in millimetres.
321
331
  `svg_stroke`/`svg_stroke_width` also pad the page, so they are not
@@ -335,7 +345,7 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
335
345
  """
336
346
  try:
337
347
  return list(_ext.export_model(path, geometry, format, ascii_stl, strip_slivers,
338
- split_components, svg_fill, svg_fill_color,
348
+ split_components, split_colors, svg_fill, svg_fill_color,
339
349
  svg_stroke, svg_stroke_color, svg_stroke_width,
340
350
  pdf_options))
341
351
  except Exception as e:
@@ -286,6 +286,14 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
286
286
  std::vector<Part2d> res2d;
287
287
  bool have2d = false;
288
288
  size_t idx = 0;
289
+ // Whether the 3D operands could possibly disagree on colour. Only then
290
+ // is attachTriColors worth its GetMeshGL(), which evaluates the boolean
291
+ // and copies the whole mesh out -- at EVERY level of a nested union
292
+ // chain, when nothing but the top ever needed it. Operands that all
293
+ // carry the same colour and no per-triangle colours of their own can
294
+ // only produce a uniformly coloured result.
295
+ std::optional<std::optional<std::array<float, 4>>> firstColor;
296
+ bool mixedColors = false;
289
297
 
290
298
  size_t stmtIndex = 0;
291
299
  for (const Value& sizeVal : groupSizes) {
@@ -314,7 +322,7 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
314
322
  allDo.insert(allDo.end(), split.displayOnly.begin(), split.displayOnly.end());
315
323
 
316
324
  std::vector<ColoredBody> bodies3d, sections2d;
317
- for (const ColoredBody& c : split.foreground) {
325
+ for (ColoredBody& c : split.foreground) {
318
326
  // A body whose own Manifold::Status() isn't NoError (e.g.
319
327
  // NonFiniteVertex, from a degenerate accumulated transform deep
320
328
  // in an unrelated ancestor's positioning math -- found via a
@@ -331,7 +339,12 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
331
339
  // operand and unioning everything else is the closest match to
332
340
  // its own behavior available without new numerical-robustness
333
341
  // work on Manifold's own boolean ops.
334
- if (c.body && c.body->Status() == manifold::Manifold::Error::NoError) bodies3d.push_back(c);
342
+ if (c.body && bodyStatus(c) == manifold::Manifold::Error::NoError) bodies3d.push_back(c);
343
+ }
344
+ for (const ColoredBody& c : bodies3d) {
345
+ if (c.triColors) mixedColors = true;
346
+ if (!firstColor) firstColor = c.color;
347
+ else if (*firstColor != c.color) mixedColors = true;
335
348
  }
336
349
  for (const ColoredBody& c : split.foreground) {
337
350
  if (c.section) sections2d.push_back(c);
@@ -358,17 +371,33 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
358
371
  if (!bodies3d.empty()) {
359
372
  manifold::Manifold grp = *bodies3d.front().body;
360
373
  for (size_t i = 1; i < bodies3d.size(); ++i) grp = grp + *bodies3d[i].body;
374
+ // The result stays LAZY inside Manifold: its emptiness is derived
375
+ // from the operands' flags here rather than asked, so a chain of
376
+ // unions under transforms is evaluated once, at the top, instead
377
+ // of once per level (see ColoredBody::knownEmpty).
378
+ const std::optional<bool> grpEmpty =
379
+ unionEmptyOf(bodies3d, [](const ColoredBody& b) -> const ColoredBody& { return b; });
380
+ const auto known = [](const std::optional<bool>& e, bool v) { return e && *e == v; };
361
381
  if (!res3d) {
362
382
  ColoredBody cb;
363
383
  cb.body = std::move(grp);
364
384
  cb.color = bodies3d.front().color;
385
+ cb.knownStatus = manifold::Manifold::Error::NoError; // every operand was
386
+ cb.knownEmpty = grpEmpty;
365
387
  res3d = std::move(cb);
366
388
  } else if (op == "union") {
367
389
  res3d->body = *res3d->body + grp;
390
+ if (known(res3d->knownEmpty, false) || known(grpEmpty, false)) res3d->knownEmpty = false;
391
+ else if (known(res3d->knownEmpty, true) && known(grpEmpty, true)) res3d->knownEmpty = true;
392
+ else res3d->knownEmpty.reset();
368
393
  } else if (op == "difference") {
369
394
  res3d->body = *res3d->body - grp;
395
+ if (known(res3d->knownEmpty, true)) res3d->knownEmpty = true;
396
+ else if (!known(grpEmpty, true)) res3d->knownEmpty.reset(); // subtracting nothing changes nothing
370
397
  } else if (op == "intersection") {
371
398
  res3d->body = *res3d->body ^ grp;
399
+ if (known(res3d->knownEmpty, true) || known(grpEmpty, true)) res3d->knownEmpty = true;
400
+ else res3d->knownEmpty.reset();
372
401
  }
373
402
  }
374
403
  if (!sections2d.empty()) {
@@ -394,7 +423,7 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
394
423
  }
395
424
  }
396
425
 
397
- if (res3d && res3d->body) attachTriColors(ev, *res3d);
426
+ if (res3d && res3d->body && mixedColors) attachTriColors(ev, *res3d);
398
427
 
399
428
  std::vector<ColoredBody> result;
400
429
  if (res3d) result.push_back(std::move(*res3d));
@@ -65,8 +65,8 @@ namespace {
65
65
  // `union() { color("red") stroke(...); text(); }` had no red recorded for
66
66
  // the stroke's runs at all, so every run looked identical, the merge kept
67
67
  // the first child's colour, and the whole thing came out red.
68
- void recordRunColors(Evaluator& ev, const ColoredBody& b, const std::optional<std::array<float, 4>>& rgba) {
69
- if (!b.body || b.body->IsEmpty()) return;
68
+ void recordRunColors(Evaluator& ev, ColoredBody& b, const std::optional<std::array<float, 4>>& rgba) {
69
+ if (!b.body || bodyIsEmpty(b)) return;
70
70
  // A body that is still one original knows its own ID without building
71
71
  // a mesh -- the common case, and the cheap one.
72
72
  const int original = b.body->OriginalID();
@@ -68,6 +68,7 @@ ColoredBody combineBodies(const std::vector<ColoredBody>& bodies) {
68
68
  ColoredBody cb;
69
69
  cb.body = std::move(merged);
70
70
  cb.color = bodies3d.front()->color;
71
+ cb.knownEmpty = unionEmptyOf(bodies3d, [](const ColoredBody* b) -> const ColoredBody& { return *b; });
71
72
  return cb;
72
73
  }
73
74
  std::vector<const ColoredBody*> sections;
@@ -34,7 +34,18 @@ CSGParams resolveText(Evaluator& ev, const oscad::ModularCall& node, EvalContext
34
34
  auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
35
35
  const std::string text = asStringOr(getArg(args, 0, "text", Value{std::string("")}), "");
36
36
  const double size = toDoubleLenient(getArg(args, 1, "size", Value{10.0}));
37
- const std::string fontSpec = asStringOr(getArg(args, std::nullopt, "font", Value{std::string("")}), "");
37
+ // font is POSITIONAL, at index 2: `text("Hi", 10, "Liberation Sans")`
38
+ // is how people write it and how the reference reads it -- measured
39
+ // against the 2026.02.01 binary, whose bounding box for the
40
+ // positional form matches the named one exactly.
41
+ //
42
+ // Only font. halign/valign/spacing/direction/language/script stay
43
+ // name-only, because the same measurement shows the reference
44
+ // ignoring them positionally: `text("Hi", 10, "F", "center")` is NOT
45
+ // centred there, while halign="center" is. text() and textmetrics()
46
+ // genuinely differ here -- textmetrics takes all nine positionally --
47
+ // so neither can be assumed from the other.
48
+ const std::string fontSpec = asStringOr(getArg(args, 2, "font", Value{std::string("")}), "");
38
49
  const std::string halign = asStringOr(getArg(args, std::nullopt, "halign", Value{std::string("left")}), "left");
39
50
  const std::string valign = asStringOr(getArg(args, std::nullopt, "valign", Value{std::string("baseline")}), "baseline");
40
51
  const double spacing = toDoubleLenient(getArg(args, std::nullopt, "spacing", Value{1.0}));