openscad-cpp-evaluator 1.19.3__tar.gz → 1.20.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 (223) hide show
  1. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/CLAUDE.md +27 -2
  2. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/bindings/module.cpp +3 -2
  4. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/colored_body.hpp +18 -0
  5. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/evaluator.hpp +16 -0
  6. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/pyproject.toml +1 -1
  7. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/python/openscad_cpp_evaluator/__init__.py +8 -2
  8. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/booleans.cpp +106 -0
  9. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/color.cpp +16 -24
  10. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/transforms.cpp +17 -0
  11. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/csg_generate.cpp +42 -0
  12. openscad_cpp_evaluator-1.20.0/tests/test_multi_color_merge.cpp +255 -0
  13. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_python_bindings.py +11 -0
  14. openscad_cpp_evaluator-1.19.3/tests/test_multi_color_merge.cpp +0 -110
  15. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/.github/workflows/ci.yml +0 -0
  16. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/.github/workflows/wheels.yml +0 -0
  17. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/.gitignore +0 -0
  18. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/.gitmodules +0 -0
  19. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/LICENSE +0 -0
  21. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/README.md +0 -0
  22. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/bindings/CMakeLists.txt +0 -0
  23. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/bindings/ast_to_py.cpp +0 -0
  24. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/cmake/embed_font.cmake +0 -0
  25. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/examples/CMakeLists.txt +0 -0
  26. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/examples/eval_perf_benchmark.cpp +0 -0
  27. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/examples/manifold_cache_reuse.cpp +0 -0
  28. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/examples/minimal_debugger.cpp +0 -0
  29. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  30. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/.gitignore +0 -0
  31. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  32. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  33. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/README.md +0 -0
  34. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  35. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  36. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  37. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  38. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  39. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  40. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  41. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  42. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  43. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  44. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  45. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  46. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  47. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  48. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  49. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  50. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  51. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  52. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  53. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  54. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  55. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  56. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  57. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  58. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  59. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  60. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  61. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  62. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  63. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  64. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  65. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  66. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  67. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  68. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  69. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  70. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  71. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  72. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  73. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  74. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  75. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  76. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  77. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  78. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  79. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  80. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  81. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  82. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  83. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  84. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  85. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  86. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  87. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  88. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  89. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  90. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  91. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  92. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  93. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  94. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  95. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  96. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  97. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  98. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  99. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  100. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  101. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  102. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  103. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  104. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  105. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  106. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  107. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  108. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  109. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  110. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  111. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  112. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  113. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  114. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  115. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  116. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  117. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  118. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  119. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  120. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  121. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  122. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  123. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  124. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  125. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  126. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  127. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  128. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  129. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  130. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  131. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  132. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LICENSE +0 -0
  133. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  134. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  135. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  136. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  137. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/.DS_Store +0 -0
  138. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/CMakeLists.txt +0 -0
  139. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/builtins.hpp +0 -0
  140. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/call_args.cpp +0 -0
  141. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/control.cpp +0 -0
  142. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/dxf_dim.cpp +0 -0
  143. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/extrude.cpp +0 -0
  144. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/function_builtins.cpp +0 -0
  145. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/import.cpp +0 -0
  146. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/modifiers.cpp +0 -0
  147. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/primitives_2d.cpp +0 -0
  148. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/primitives_3d.cpp +0 -0
  149. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/registry.cpp +0 -0
  150. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/roof.cpp +0 -0
  151. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/segments.cpp +0 -0
  152. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/surface.cpp +0 -0
  153. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/text.cpp +0 -0
  154. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/builtins/topology.cpp +0 -0
  155. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/bytecode_compiler.cpp +0 -0
  156. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/bytecode_vm.cpp +0 -0
  157. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/colored_body.cpp +0 -0
  158. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/coverage.cpp +0 -0
  159. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/csg_resolve.cpp +0 -0
  160. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/css_colors.cpp +0 -0
  161. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/debug/debug_repl.cpp +0 -0
  162. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/debug_profile.cpp +0 -0
  163. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/eval_context.cpp +0 -0
  164. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/eval_error.cpp +0 -0
  165. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/eval_use.cpp +0 -0
  166. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/export.cpp +0 -0
  167. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/expr_eval.cpp +0 -0
  168. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/format_closure.cpp +0 -0
  169. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/import/dxf_import.cpp +0 -0
  170. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/import/mesh_import.cpp +0 -0
  171. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/import/surface_load.cpp +0 -0
  172. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/import/svg_import.cpp +0 -0
  173. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/manifold_cache.cpp +0 -0
  174. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/measure_geometry.cpp +0 -0
  175. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/mesh_check.cpp +0 -0
  176. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/stmt_eval.cpp +0 -0
  177. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/text/font_match.cpp +0 -0
  178. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/text/freetype_font_provider.cpp +0 -0
  179. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/text/text_metrics.cpp +0 -0
  180. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/user_calls.cpp +0 -0
  181. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/utf8.cpp +0 -0
  182. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/value.cpp +0 -0
  183. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/src/zip_stored.cpp +0 -0
  184. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/CMakeLists.txt +0 -0
  185. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_booleans.cpp +0 -0
  186. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_bytecode_compiler.cpp +0 -0
  187. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_children_separate.cpp +0 -0
  188. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_cli.cpp +0 -0
  189. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_control_flow.cpp +0 -0
  190. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_coverage.cpp +0 -0
  191. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_csg_tree.cpp +0 -0
  192. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_debug_hooks.cpp +0 -0
  193. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_dxf_svg_import.cpp +0 -0
  194. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_eval_context.cpp +0 -0
  195. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_export.cpp +0 -0
  196. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_expr_eval.cpp +0 -0
  197. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_extrude_roof.cpp +0 -0
  198. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_feature_detection.cpp +0 -0
  199. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_function_builtins.cpp +0 -0
  200. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_helpers.hpp +0 -0
  201. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_import_export.cpp +0 -0
  202. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_include_cache.cpp +0 -0
  203. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_manifold_cache.cpp +0 -0
  204. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_mesh_check.cpp +0 -0
  205. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_primitives.cpp +0 -0
  206. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_profiling.cpp +0 -0
  207. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_render_expr.cpp +0 -0
  208. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_scope_trail.cpp +0 -0
  209. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_scoping.cpp +0 -0
  210. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_surface.cpp +0 -0
  211. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_tail_calls.cpp +0 -0
  212. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_text.cpp +0 -0
  213. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_transforms.cpp +0 -0
  214. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_unexpected_args.cpp +0 -0
  215. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_use.cpp +0 -0
  216. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_value.cpp +0 -0
  217. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_viewport_params.cpp +0 -0
  218. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tests/test_zip_stored.cpp +0 -0
  219. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tools/cli/CMakeLists.txt +0 -0
  220. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tools/cli/cli_lib.cpp +0 -0
  221. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tools/cli/cli_lib.hpp +0 -0
  222. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/tools/cli/main.cpp +0 -0
  223. {openscad_cpp_evaluator-1.19.3 → openscad_cpp_evaluator-1.20.0}/uv.lock +0 -0
@@ -866,8 +866,33 @@ grep for `ponytail:`.
866
866
  And the invariant behind both: **`triColors` must index the body's CURRENT mesh**. The renderer
867
867
  masks its vertex arrays with it, so a stale length is an `IndexError` and a blank viewport rather
868
868
  than a wrong colour. Anything that re-meshes clears it — `simplify()`, `minkowski_difference()`,
869
- and the ID-retag rebuild when the triangle count moves — and `bodyToDict` drops a mismatched array
870
- as a backstop for whichever one gets missed next.
869
+ and `bodyToDict` drops a mismatched array as a backstop for whichever one gets missed next. The
870
+ ID-retag rebuild on a cache hit (`restampCachedIds`) used to clear it too when the triangle count
871
+ moved; it now **rebuilds it from the runs**, which survive the rebuild, and **re-records every
872
+ run's colour against the fresh IDs** from the cached body itself (its `color`, or per run what its
873
+ `triColors` carry) — this render's `idToColor` has never heard of an ID an earlier render minted,
874
+ and without it every cached operand looked uncoloured to the next merge: an edited three-colour
875
+ `difference()` lost the unchanged subtrahend's colour on the first re-render and went flat on the
876
+ next (BelfrySCAD #412).
877
+ **An uncoloured subtrahend paints the faces it exposes the cut green** (`kCutFaceColor`,
878
+ `colored_body.hpp`: #9DCB51, the reference's CGAL back-face green, which its preview shows and its
879
+ colour-preserving render keeps — measured in a 3MF export from 2026.02.01), recorded against the
880
+ tool's runs before the merge, and the merge is forced to look even when every operand's own colour
881
+ agrees. It used to fall to the default geometry colour, so a cut through an uncoloured part could
882
+ not be seen as a cut. A coloured tool still paints its cut with its own colour, as the reference
883
+ does.
884
+ **`Evaluator::keepMinuendColor`** (bindings: `Evaluator(keep_minuend_color=True)`) is the viewer
885
+ option the reference cannot offer (openscad/openscad#4798 — OpenCSG preview is pixels in a frame
886
+ buffer): `difference()` paints its cut faces with the **minuend's** colour instead. Since
887
+ (A ∪ B) − S = (A − S) ∪ (B − S), each minuend part is differenced on its own and its cut-face runs
888
+ are re-minted under fresh IDs carrying that part's colour (`finishKeepMinuend`, `booleans.cpp`; the
889
+ source node stays the tool's, so a click on a cut face still finds it), then the parts are unioned.
890
+ A `union()` built in this mode remembers what it merged (`ColoredBody::mergedFrom`, unmerged and
891
+ carried through `generateTransform`/`generateColor`/`restampCachedIds`), so `difference() {
892
+ union() { red; blue; } tool }` cuts red on the red side and blue on the blue side; any op that builds
893
+ a new body drops the record and that body is one part. Off by default — a viewer option, not a
894
+ language feature, so it is honest for every existing script — and cache keys are prefixed per
895
+ mode so the two never serve each other's bodies. Costs one boolean per minuend part.
871
896
  **2D cannot use any of that, and keeps colour geometrically instead** (`Part2d`, same file): a
872
897
  `CrossSection` is contours, not a mesh, so nothing in it remembers which child an edge came from
873
898
  and there is no provenance for `attachTriColors` to read back. So the 2D accumulator holds **one
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.19.3
3
+ Version: 1.20.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -477,7 +477,7 @@ nb::object coverageResultToPy(const std::optional<oscadeval::CoverageResult>& cr
477
477
 
478
478
  nb::object evaluate(const std::string& path, nb::dict viewportParams,
479
479
  std::shared_ptr<oscadeval::ManifoldCache> manifoldCache, bool profile,
480
- bool generate, bool strictCommas, bool coverage) {
480
+ bool generate, bool strictCommas, bool coverage, bool keepMinuendColor) {
481
481
  std::unordered_map<std::string, oscadeval::Value> vp = toViewportParams(viewportParams);
482
482
 
483
483
  std::vector<oscadeval::ColoredBody> bodies;
@@ -501,6 +501,7 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
501
501
  oscad::ParsedProgram program = oscad::getProgramFromFile(path);
502
502
  oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, logFn);
503
503
  oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile, coverage);
504
+ ev.keepMinuendColor = keepMinuendColor;
504
505
  ev.setUsedFileGlobals(used.usedFileGlobals);
505
506
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
506
507
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp, generate));
@@ -955,7 +956,7 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
955
956
 
956
957
  m.def("evaluate", &evaluate, nb::arg("path"), nb::arg("viewport_params"), nb::arg("manifold_cache") = nullptr,
957
958
  nb::arg("profile") = false, nb::arg("generate") = true, nb::arg("strict_commas") = false,
958
- nb::arg("coverage") = false,
959
+ nb::arg("coverage") = false, nb::arg("keep_minuend_color") = false,
959
960
  "Evaluate a .scad file; return (bodies, echoes, id_to_node, csg_tree, profile_result, dyn, dyn_explicit, "
960
961
  "geometry, coverage_result).\n"
961
962
  "coverage=True records which statements, branch arms and bodies ran: coverage_result is a dict "
@@ -6,6 +6,7 @@
6
6
  #include <manifold/manifold.h>
7
7
 
8
8
  #include <array>
9
+ #include <memory>
9
10
  #include <optional>
10
11
  #include <vector>
11
12
 
@@ -28,6 +29,13 @@ enum class BodyRole { Normal, Highlight, Background, ShowOnly };
28
29
  // Matches the reference's SceneRenderer._default_color.
29
30
  inline constexpr std::array<float, 4> kDefaultGeometryColor{0.9f, 0.85f, 0.1f, 1.0f};
30
31
 
32
+ // The colour of a face a difference() exposed when the subtrahend that cut
33
+ // it carried no colour of its own: the reference's CGAL "back face" green,
34
+ // the same one its preview paints and its colour-preserving render (and
35
+ // 3MF export, measured on 2026.02.01) keeps. Distinct from the default
36
+ // geometry colour so a cut reads as a cut.
37
+ inline constexpr std::array<float, 4> kCutFaceColor{157.0f / 255.0f, 203.0f / 255.0f, 81.0f / 255.0f, 1.0f};
38
+
31
39
  struct ColoredBody {
32
40
  std::optional<manifold::Manifold> body;
33
41
  std::optional<std::array<float, 4>> color; // RGBA; nullopt = "no explicit color() -- follow the live theme"
@@ -58,6 +66,16 @@ struct ColoredBody {
58
66
  BodyRole role = BodyRole::Normal;
59
67
  std::optional<std::vector<std::array<float, 4>>> triColors; // per-triangle RGBA, multi-color CSG merges only
60
68
 
69
+ // The operands a union() merged into this body, kept unmerged, each as
70
+ // it was when merged (colour and transforms carried along since --
71
+ // generateTransform and generateColor map over them). Set only under
72
+ // Evaluator::keepMinuendColor, where a difference() cuts each part on
73
+ // its own so its cut faces take THAT part's colour; nothing else reads
74
+ // it, and any other op that builds a new body from this one drops it.
75
+ // Shared, never mutated in place: an op that changes it makes a new
76
+ // vector.
77
+ std::shared_ptr<const std::vector<ColoredBody>> mergedFrom;
78
+
61
79
  // Whether `body` is empty, once anything has asked (isEmptyBody,
62
80
  // csg_generate.cpp). Asking Manifold is not free: every accessor goes
63
81
  // through GetImpl(), which MATERIALIZES a lazy transform (a full copy of
@@ -294,6 +294,22 @@ public:
294
294
  std::size_t generatedNodeCount = 0;
295
295
  std::unordered_map<uint32_t, std::optional<std::array<float, 4>>> idToColor;
296
296
 
297
+ // Record `rgba` against every run ID of `b`, so a later merge can still
298
+ // tell the body's triangles apart once the body itself is gone
299
+ // (attachTriColors looks runs up in idToColor). Cheap for a body that is
300
+ // still one original; a merged body pays a GetMeshGL().
301
+ void recordRunColors(ColoredBody& b, const std::optional<std::array<float, 4>>& rgba);
302
+
303
+ // difference() keeps the minuend's colour on the faces a subtrahend
304
+ // exposes, instead of the subtrahend's colour (or the cut green). Off
305
+ // by default: OpenSCAD paints cut faces with the cutter's colour, and
306
+ // that is what a script author sees there. A viewer option, not a
307
+ // language feature -- it is honest for every existing script. Costs one
308
+ // boolean per coloured minuend part instead of one per difference,
309
+ // since (A ∪ B) − S = (A − S) ∪ (B − S) is how each part's cut faces get
310
+ // that part's colour. Cached results are keyed apart per mode.
311
+ bool keepMinuendColor = false;
312
+
297
313
  // Called by primitive-construction generate functions (cube, sphere,
298
314
  // cylinder, polyhedron) right after building a brand-new Manifold:
299
315
  // reads back its mesh's runOriginalID run(s) and records each against
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.19.3"
7
+ version = "1.20.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -532,7 +532,8 @@ class Evaluator:
532
532
  """
533
533
 
534
534
  def __init__(self, echo_fn=None, debug_hook=None, error_break_fn=None, return_hook=None,
535
- manifold_cache=None, profile=False, fast_continue_signal=None, coverage=False):
535
+ manifold_cache=None, profile=False, fast_continue_signal=None, coverage=False,
536
+ keep_minuend_color=False):
536
537
  self._echo_fn = echo_fn
537
538
  self._debug_hook = debug_hook
538
539
  self._error_break_fn = error_break_fn
@@ -541,6 +542,11 @@ class Evaluator:
541
542
  self._profile = profile
542
543
  self._fast_continue_signal = fast_continue_signal
543
544
  self._coverage = coverage
545
+ # keep_minuend_color=True: difference() paints the faces a
546
+ # subtrahend exposes with the MINUEND's colour rather than the
547
+ # subtrahend's (or the cut green). A viewer option; see
548
+ # Evaluator::keepMinuendColor in evaluator.hpp.
549
+ self._keep_minuend_color = keep_minuend_color
544
550
  self.csg_tree = []
545
551
  self.profile_result = None
546
552
  # coverage=True: after evaluate(), {"spans": [...], "files": [...],
@@ -594,7 +600,7 @@ class Evaluator:
594
600
  (body_dicts, echoes, id_spans, csg_tree, profile_result, dyn,
595
601
  dyn_explicit, geometry, coverage_result) = _ext.evaluate(
596
602
  source_path, vp, self._manifold_cache, self._profile, generate,
597
- strict_commas, self._coverage)
603
+ strict_commas, self._coverage, self._keep_minuend_color)
598
604
  # The evaluated bodies, still on the C++ side. Stashed like
599
605
  # csg_tree/profile_result rather than returned, so
600
606
  # evaluate()'s own 2-tuple result is unchanged -- callers
@@ -7,6 +7,7 @@
7
7
  #include <array>
8
8
  #include <cstdint>
9
9
  #include <optional>
10
+ #include <unordered_map>
10
11
 
11
12
  namespace oscadeval {
12
13
 
@@ -199,6 +200,61 @@ void attachTriColors(Evaluator& ev, ColoredBody& cb) {
199
200
  cb.triColors = std::move(triColors);
200
201
  }
201
202
 
203
+ // keepMinuendColor: the leaf parts of a minuend operand -- the operand
204
+ // itself, or what a union() merged it from, recursively.
205
+ void collectKeepParts(const ColoredBody& b, std::vector<ColoredBody>& out) {
206
+ if (b.mergedFrom) {
207
+ for (const ColoredBody& part : *b.mergedFrom) collectKeepParts(part, out);
208
+ } else if (b.body) {
209
+ out.push_back(b);
210
+ }
211
+ }
212
+
213
+ // keepMinuendColor: one minuend part, differenced on its own.
214
+ struct KeepPart {
215
+ ColoredBody body;
216
+ std::vector<uint32_t> ownIds; // run IDs the part was born with; anything else on it afterwards is a cut face
217
+ };
218
+
219
+ std::vector<uint32_t> runIdsOf(const ColoredBody& b) {
220
+ const int original = b.body->OriginalID();
221
+ if (original >= 0) return {static_cast<uint32_t>(original)};
222
+ return b.body->GetMeshGL().runOriginalID;
223
+ }
224
+
225
+ // Union the per-part differences back into one body, first re-minting each
226
+ // part's cut-face runs (the subtrahend's IDs, shared by every part's result)
227
+ // under fresh IDs carrying THAT part's colour, so attachTriColors can tell
228
+ // one part's cut faces from another's. The source node stays the
229
+ // subtrahend's: clicking a cut face still finds the tool that made it.
230
+ manifold::Manifold finishKeepMinuend(Evaluator& ev, std::vector<KeepPart>& parts) {
231
+ std::optional<manifold::Manifold> out;
232
+ for (KeepPart& part : parts) {
233
+ if (!part.body.body) continue;
234
+ manifold::MeshGL mesh = part.body.body->GetMeshGL();
235
+ if (mesh.triVerts.empty()) continue;
236
+ std::unordered_map<uint32_t, uint32_t> remap;
237
+ for (uint32_t& id : mesh.runOriginalID) {
238
+ if (std::find(part.ownIds.begin(), part.ownIds.end(), id) != part.ownIds.end()) continue;
239
+ auto found = remap.find(id);
240
+ if (found == remap.end()) {
241
+ const uint32_t fresh = manifold::Manifold::ReserveIDs(1);
242
+ auto node = ev.idToNode.find(id);
243
+ if (node != ev.idToNode.end()) ev.idToNode[fresh] = node->second;
244
+ // ponytail: a part that is itself a multi-colour merge
245
+ // gives its cut faces its first child's colour rather than
246
+ // the colour of whichever child the cut passed through.
247
+ ev.idToColor[fresh] = part.body.color;
248
+ found = remap.emplace(id, fresh).first;
249
+ }
250
+ id = found->second;
251
+ }
252
+ manifold::Manifold rebuilt(mesh);
253
+ out = out ? *out + rebuilt : rebuilt;
254
+ }
255
+ return out.value_or(manifold::Manifold());
256
+ }
257
+
202
258
  // One colour's worth of 2D result.
203
259
  //
204
260
  // 3D recovers per-child colour AFTER the merge, from Manifold's own
@@ -286,6 +342,15 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
286
342
  // only produce a uniformly coloured result.
287
343
  std::optional<std::optional<std::array<float, 4>>> firstColor;
288
344
  bool mixedColors = false;
345
+ // keepMinuendColor: the minuend's parts, each differenced on its own
346
+ // (see Evaluator::keepMinuendColor). Not while measuring: a render()
347
+ // expression only wants the volume, which is the same either way.
348
+ const bool keeping = op == "difference" && ev.keepMinuendColor && !ev.measuring();
349
+ std::vector<KeepPart> keepParts;
350
+ // ... and a union() built under that mode remembers what it merged, so
351
+ // a difference() it is the minuend of can cut each part on its own.
352
+ const bool rememberParts = op == "union" && ev.keepMinuendColor && !ev.measuring();
353
+ std::vector<ColoredBody> unionParts;
289
354
 
290
355
  size_t stmtIndex = 0;
291
356
  for (const Value& sizeVal : groupSizes) {
@@ -338,6 +403,20 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
338
403
  if (!firstColor) firstColor = c.color;
339
404
  else if (*firstColor != c.color) mixedColors = true;
340
405
  }
406
+ // A subtrahend with no colour of its own paints the faces it
407
+ // exposes the cut green, as the reference does, rather than the
408
+ // default geometry colour -- otherwise a cut through an uncoloured
409
+ // part is invisible as a cut. Recorded against its runs, which is
410
+ // all attachTriColors will have left after the merge; and the
411
+ // merge must then look, even when every operand's own colour
412
+ // agrees.
413
+ if (op == "difference" && res3d && !ev.measuring()) {
414
+ for (ColoredBody& c : bodies3d) {
415
+ if (c.color || c.triColors) continue;
416
+ ev.recordRunColors(c, kCutFaceColor);
417
+ mixedColors = true;
418
+ }
419
+ }
341
420
  for (const ColoredBody& c : split.foreground) {
342
421
  if (c.section) sections2d.push_back(c);
343
422
  }
@@ -377,8 +456,27 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
377
456
  cb.knownStatus = manifold::Manifold::Error::NoError; // every operand was
378
457
  cb.knownEmpty = grpEmpty;
379
458
  res3d = std::move(cb);
459
+ if (keeping) {
460
+ std::vector<ColoredBody> leaves;
461
+ for (const ColoredBody& c : bodies3d) collectKeepParts(c, leaves);
462
+ for (ColoredBody& leaf : leaves) {
463
+ std::vector<uint32_t> ids = runIdsOf(leaf);
464
+ keepParts.push_back({std::move(leaf), std::move(ids)});
465
+ }
466
+ }
467
+ if (rememberParts) {
468
+ for (const ColoredBody& c : bodies3d) collectKeepParts(c, unionParts);
469
+ }
470
+ } else if (keeping) {
471
+ // The whole-minuend result above is never evaluated (Manifold
472
+ // is lazy); finishKeepMinuend replaces it after the loop.
473
+ for (KeepPart& part : keepParts) part.body.body = *part.body.body - grp;
474
+ res3d->knownEmpty.reset();
380
475
  } else if (op == "union") {
381
476
  res3d->body = *res3d->body + grp;
477
+ if (rememberParts) {
478
+ for (const ColoredBody& c : bodies3d) collectKeepParts(c, unionParts);
479
+ }
382
480
  if (known(res3d->knownEmpty, false) || known(grpEmpty, false)) res3d->knownEmpty = false;
383
481
  else if (known(res3d->knownEmpty, true) && known(grpEmpty, true)) res3d->knownEmpty = true;
384
482
  else res3d->knownEmpty.reset();
@@ -415,6 +513,14 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
415
513
  }
416
514
  }
417
515
 
516
+ if (res3d && res3d->body && rememberParts && unionParts.size() > 1) {
517
+ res3d->mergedFrom = std::make_shared<const std::vector<ColoredBody>>(std::move(unionParts));
518
+ }
519
+ if (res3d && res3d->body && keeping && !keepParts.empty()) {
520
+ res3d->body = finishKeepMinuend(ev, keepParts);
521
+ res3d->knownEmpty.reset();
522
+ mixedColors = true; // attachTriColors still no-ops when every run agrees
523
+ }
418
524
  if (res3d && res3d->body && mixedColors) attachTriColors(ev, *res3d);
419
525
 
420
526
  std::vector<ColoredBody> result;
@@ -52,31 +52,19 @@ CSGParams resolveColor(Evaluator& ev, const oscad::ModularCall& node, EvalContex
52
52
 
53
53
  namespace {
54
54
 
55
- // Stamping cb.color is not enough: the colour has to be recorded against
56
- // the body's RUN IDS too.
57
- //
58
- // attachTriColors (booleans.cpp) reads `idToColor` back AFTER a merge has
59
- // thrown the individual bodies away -- run IDs are all it has left to go
60
- // on -- and a run it cannot find looks uncoloured. tagGenerated() records
61
- // the colour a body was BORN with, which covers `color("red") cube(10)`
62
- // because the colour reaches the primitive through the context. It does
63
- // not cover a body that already existed when colour was applied to it,
64
- // which is every module whose geometry comes out of its own CSG:
65
- // `union() { color("red") stroke(...); text(); }` had no red recorded for
66
- // the stroke's runs at all, so every run looked identical, the merge kept
67
- // the first child's colour, and the whole thing came out red.
68
- void recordRunColors(Evaluator& ev, ColoredBody& b, const std::optional<std::array<float, 4>>& rgba) {
69
- if (!b.body || bodyIsEmpty(b)) return;
70
- // A body that is still one original knows its own ID without building
71
- // a mesh -- the common case, and the cheap one.
72
- const int original = b.body->OriginalID();
73
- if (original >= 0) {
74
- ev.idToColor[static_cast<uint32_t>(original)] = rgba;
75
- return;
55
+ } // namespace
56
+
57
+ namespace {
58
+ std::shared_ptr<const std::vector<ColoredBody>> recolorParts(const std::vector<ColoredBody>& parts,
59
+ const std::optional<std::array<float, 4>>& rgba) {
60
+ std::vector<ColoredBody> out;
61
+ for (ColoredBody part : parts) {
62
+ part.color = rgba;
63
+ if (part.mergedFrom) part.mergedFrom = recolorParts(*part.mergedFrom, rgba);
64
+ out.push_back(std::move(part));
76
65
  }
77
- for (uint32_t id : b.body->GetMeshGL().runOriginalID) ev.idToColor[id] = rgba;
66
+ return std::make_shared<const std::vector<ColoredBody>>(std::move(out));
78
67
  }
79
-
80
68
  } // namespace
81
69
 
82
70
  std::vector<ColoredBody> generateColor(Evaluator& ev, const CSGParams& params,
@@ -85,7 +73,11 @@ std::vector<ColoredBody> generateColor(Evaluator& ev, const CSGParams& params,
85
73
  std::vector<ColoredBody> result;
86
74
  for (ColoredBody b : flattenCsgTree(children)) {
87
75
  b.color = rgba;
88
- if (!ev.measuring()) recordRunColors(ev, b, rgba);
76
+ if (!ev.measuring()) ev.recordRunColors(b, rgba);
77
+ // color() over a union() colours every part it was merged from
78
+ // (see ColoredBody::mergedFrom) -- the parts' runs are the body's
79
+ // runs, so recording once above already covers them.
80
+ if (b.mergedFrom) b.mergedFrom = recolorParts(*b.mergedFrom, rgba);
89
81
  result.push_back(std::move(b));
90
82
  }
91
83
  return result;
@@ -349,6 +349,19 @@ CSGParams resolveTransform(Evaluator& ev, const oscad::ModularCall& node, EvalCo
349
349
  return std::move(result.params);
350
350
  }
351
351
 
352
+ namespace {
353
+ std::shared_ptr<const std::vector<ColoredBody>> transformParts(const std::vector<ColoredBody>& parts,
354
+ const std::string& name, const CallArgs& args) {
355
+ std::vector<ColoredBody> out;
356
+ for (ColoredBody part : parts) {
357
+ if (part.body) part.body = applyTransform3d(name, args, std::move(*part.body));
358
+ if (part.mergedFrom) part.mergedFrom = transformParts(*part.mergedFrom, name, args);
359
+ out.push_back(std::move(part));
360
+ }
361
+ return std::make_shared<const std::vector<ColoredBody>>(std::move(out));
362
+ }
363
+ } // namespace
364
+
352
365
  std::vector<ColoredBody> generateTransform(Evaluator&, const CSGParams& params,
353
366
  const std::vector<std::unique_ptr<CSGNode>>& children, const oscad::ASTNode&) {
354
367
  const std::string& name = std::get<std::string>(params.at("name"));
@@ -391,6 +404,10 @@ std::vector<ColoredBody> generateTransform(Evaluator&, const CSGParams& params,
391
404
  if (std::optional<manifold::mat3x4> m = transformMatrix3d(name, args))
392
405
  transformMeshInPlace(*b.rawMesh, *m);
393
406
  }
407
+ // The unmerged parts of a union move with it (see
408
+ // ColoredBody::mergedFrom). Recursion through this same
409
+ // function keeps their own parts moving too.
410
+ if (b.mergedFrom) b.mergedFrom = transformParts(*b.mergedFrom, name, args);
394
411
  }
395
412
  result.push_back(std::move(b));
396
413
  }
@@ -46,6 +46,11 @@ std::vector<ColoredBody> Evaluator::generateTreeImpl(const std::vector<CSGNode*>
46
46
  std::optional<std::string> key;
47
47
  if (manifoldCache_ && !node.uncacheable) {
48
48
  key = cacheKey(node);
49
+ // A difference() built with keepMinuendColor colours its cut
50
+ // faces differently, so the two modes must never serve each
51
+ // other's bodies. Prefixing every key (not only differences)
52
+ // keeps the rule simple; the cache holds both.
53
+ if (keepMinuendColor) key->insert(0, "keepMinuendColor;");
49
54
  // A key that serialized a function literal is not a pure
50
55
  // function of content -- it embeds a raw AST address that the
51
56
  // next parse can hand out again. Dropping the key here forces
@@ -305,6 +310,26 @@ std::vector<ColoredBody> Evaluator::generatePartialTree() {
305
310
  return generateTreeImpl(flat);
306
311
  }
307
312
 
313
+ namespace {
314
+ std::shared_ptr<const std::vector<ColoredBody>> remapParts(const std::vector<ColoredBody>& parts,
315
+ const std::unordered_map<uint32_t, uint32_t>& remap) {
316
+ std::vector<ColoredBody> out;
317
+ for (ColoredBody part : parts) {
318
+ if (part.mergedFrom) part.mergedFrom = remapParts(*part.mergedFrom, remap);
319
+ if (part.body && !part.body->IsEmpty()) {
320
+ manifold::MeshGL mesh = part.body->GetMeshGL();
321
+ for (uint32_t& id : mesh.runOriginalID) {
322
+ auto found = remap.find(id);
323
+ if (found != remap.end()) id = found->second;
324
+ }
325
+ part.body = manifold::Manifold(mesh);
326
+ }
327
+ out.push_back(std::move(part));
328
+ }
329
+ return std::make_shared<const std::vector<ColoredBody>>(std::move(out));
330
+ }
331
+ } // namespace
332
+
308
333
  void Evaluator::restampCachedIds(std::vector<ColoredBody>& bodies, const oscad::ASTNode& node,
309
334
  const oscad::ASTNode* producer) {
310
335
  // A cache hit hands back the geometry AND the originalIDs of whichever
@@ -382,6 +407,10 @@ void Evaluator::restampCachedIds(std::vector<ColoredBody>& bodies, const oscad::
382
407
  id = found->second;
383
408
  }
384
409
  cb.body = manifold::Manifold(mesh);
410
+ // The unmerged parts a union() remembered (ColoredBody::mergedFrom)
411
+ // share the merged body's IDs, so the same remap applies to them;
412
+ // a part left on the old IDs would look uncoloured next to it.
413
+ if (cb.mergedFrom) cb.mergedFrom = remapParts(*cb.mergedFrom, remap);
385
414
  // Rebuilding a Manifold from a mesh can clean it up (degenerate
386
415
  // triangles merged away), and triColors is indexed by triangle, so
387
416
  // the old array no longer lines up. The RUNS survive the rebuild,
@@ -404,6 +433,19 @@ void Evaluator::restampCachedIds(std::vector<ColoredBody>& bodies, const oscad::
404
433
  }
405
434
  }
406
435
 
436
+
437
+ void Evaluator::recordRunColors(ColoredBody& b, const std::optional<std::array<float, 4>>& rgba) {
438
+ if (!b.body || bodyIsEmpty(b)) return;
439
+ // A body that is still one original knows its own ID without building
440
+ // a mesh -- the common case, and the cheap one.
441
+ const int original = b.body->OriginalID();
442
+ if (original >= 0) {
443
+ idToColor[static_cast<uint32_t>(original)] = rgba;
444
+ return;
445
+ }
446
+ for (uint32_t id : b.body->GetMeshGL().runOriginalID) idToColor[id] = rgba;
447
+ }
448
+
407
449
  ColoredBody Evaluator::tagGenerated(manifold::Manifold body, const oscad::ASTNode& node, const Value& colorValue) {
408
450
  manifold::MeshGL mesh = body.GetMeshGL();
409
451
  std::optional<std::array<float, 4>> color = valueToColor(colorValue);