openscad-cpp-evaluator 1.19.2__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.2 → openscad_cpp_evaluator-1.20.0}/CLAUDE.md +27 -2
  2. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/bindings/module.cpp +3 -2
  4. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/colored_body.hpp +23 -0
  5. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/evaluator.hpp +16 -0
  6. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/pyproject.toml +1 -1
  7. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/python/openscad_cpp_evaluator/__init__.py +8 -2
  8. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/booleans.cpp +106 -8
  9. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/color.cpp +16 -24
  10. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/transforms.cpp +17 -0
  11. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/csg_generate.cpp +91 -9
  12. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_manifold_cache.cpp +41 -0
  13. openscad_cpp_evaluator-1.20.0/tests/test_multi_color_merge.cpp +255 -0
  14. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_python_bindings.py +11 -0
  15. openscad_cpp_evaluator-1.19.2/tests/test_multi_color_merge.cpp +0 -111
  16. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/.github/workflows/ci.yml +0 -0
  17. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/.github/workflows/wheels.yml +0 -0
  18. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/.gitignore +0 -0
  19. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/.gitmodules +0 -0
  20. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/CMakeLists.txt +0 -0
  21. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/LICENSE +0 -0
  22. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/README.md +0 -0
  23. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/bindings/CMakeLists.txt +0 -0
  24. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/bindings/ast_to_py.cpp +0 -0
  25. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/cmake/embed_font.cmake +0 -0
  26. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/examples/CMakeLists.txt +0 -0
  27. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/examples/eval_perf_benchmark.cpp +0 -0
  28. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/examples/manifold_cache_reuse.cpp +0 -0
  29. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/examples/minimal_debugger.cpp +0 -0
  30. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  31. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/.gitignore +0 -0
  32. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  33. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  34. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/README.md +0 -0
  35. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  36. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  37. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  38. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  39. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  40. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  41. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  42. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  43. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  44. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  45. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  46. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  47. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  48. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  49. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  50. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  51. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  52. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  53. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  54. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  55. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  56. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  57. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  58. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  59. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  60. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  61. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  62. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  63. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  64. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  65. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  66. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  67. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  68. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  69. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  70. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  71. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  72. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  73. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  74. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  75. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  76. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  77. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  78. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  79. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  80. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  81. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  82. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  83. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  84. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  85. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  86. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  87. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  88. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  89. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  90. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  91. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  92. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  93. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  94. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  95. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  96. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  97. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  98. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  99. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  100. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  101. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  102. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  103. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  104. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  105. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  106. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  107. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  108. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  109. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  110. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  111. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  112. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  113. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  114. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  115. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  116. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  117. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  118. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  119. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  120. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  121. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  122. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  123. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  124. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  125. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  126. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  127. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  128. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  129. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  130. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  131. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  132. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  133. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LICENSE +0 -0
  134. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  135. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  136. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  137. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  138. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/.DS_Store +0 -0
  139. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/CMakeLists.txt +0 -0
  140. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/builtins.hpp +0 -0
  141. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/call_args.cpp +0 -0
  142. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/control.cpp +0 -0
  143. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/dxf_dim.cpp +0 -0
  144. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/extrude.cpp +0 -0
  145. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/function_builtins.cpp +0 -0
  146. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/import.cpp +0 -0
  147. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/modifiers.cpp +0 -0
  148. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/primitives_2d.cpp +0 -0
  149. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/primitives_3d.cpp +0 -0
  150. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/registry.cpp +0 -0
  151. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/roof.cpp +0 -0
  152. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/segments.cpp +0 -0
  153. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/surface.cpp +0 -0
  154. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/text.cpp +0 -0
  155. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/builtins/topology.cpp +0 -0
  156. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/bytecode_compiler.cpp +0 -0
  157. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/bytecode_vm.cpp +0 -0
  158. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/colored_body.cpp +0 -0
  159. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/coverage.cpp +0 -0
  160. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/csg_resolve.cpp +0 -0
  161. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/css_colors.cpp +0 -0
  162. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/debug/debug_repl.cpp +0 -0
  163. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/debug_profile.cpp +0 -0
  164. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/eval_context.cpp +0 -0
  165. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/eval_error.cpp +0 -0
  166. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/eval_use.cpp +0 -0
  167. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/export.cpp +0 -0
  168. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/expr_eval.cpp +0 -0
  169. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/format_closure.cpp +0 -0
  170. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/import/dxf_import.cpp +0 -0
  171. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/import/mesh_import.cpp +0 -0
  172. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/import/surface_load.cpp +0 -0
  173. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/import/svg_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/manifold_cache.cpp +0 -0
  175. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/measure_geometry.cpp +0 -0
  176. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/mesh_check.cpp +0 -0
  177. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/stmt_eval.cpp +0 -0
  178. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/text/font_match.cpp +0 -0
  179. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/text/freetype_font_provider.cpp +0 -0
  180. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/text/text_metrics.cpp +0 -0
  181. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/user_calls.cpp +0 -0
  182. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/utf8.cpp +0 -0
  183. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/value.cpp +0 -0
  184. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/src/zip_stored.cpp +0 -0
  185. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/CMakeLists.txt +0 -0
  186. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_booleans.cpp +0 -0
  187. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_bytecode_compiler.cpp +0 -0
  188. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_children_separate.cpp +0 -0
  189. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_cli.cpp +0 -0
  190. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_control_flow.cpp +0 -0
  191. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_coverage.cpp +0 -0
  192. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_csg_tree.cpp +0 -0
  193. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_debug_hooks.cpp +0 -0
  194. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_dxf_svg_import.cpp +0 -0
  195. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_eval_context.cpp +0 -0
  196. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_export.cpp +0 -0
  197. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_expr_eval.cpp +0 -0
  198. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_extrude_roof.cpp +0 -0
  199. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_feature_detection.cpp +0 -0
  200. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_function_builtins.cpp +0 -0
  201. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_helpers.hpp +0 -0
  202. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_import_export.cpp +0 -0
  203. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_include_cache.cpp +0 -0
  204. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_mesh_check.cpp +0 -0
  205. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_primitives.cpp +0 -0
  206. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_profiling.cpp +0 -0
  207. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_render_expr.cpp +0 -0
  208. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_scope_trail.cpp +0 -0
  209. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_scoping.cpp +0 -0
  210. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_surface.cpp +0 -0
  211. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_tail_calls.cpp +0 -0
  212. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_text.cpp +0 -0
  213. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_transforms.cpp +0 -0
  214. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_unexpected_args.cpp +0 -0
  215. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_use.cpp +0 -0
  216. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_value.cpp +0 -0
  217. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_viewport_params.cpp +0 -0
  218. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tests/test_zip_stored.cpp +0 -0
  219. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tools/cli/CMakeLists.txt +0 -0
  220. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tools/cli/cli_lib.cpp +0 -0
  221. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tools/cli/cli_lib.hpp +0 -0
  222. {openscad_cpp_evaluator-1.19.2 → openscad_cpp_evaluator-1.20.0}/tools/cli/main.cpp +0 -0
  223. {openscad_cpp_evaluator-1.19.2 → 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.2
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
 
@@ -23,6 +24,18 @@ enum class BodyRole { Normal, Highlight, Background, ShowOnly };
23
24
  // `role`/`triColors` are later-phase fields (modifier tagging -- Phase 3;
24
25
  // multi-color CSG merges -- Phase 3/8) included now so the struct shape
25
26
  // doesn't need reworking when those land.
27
+ // The colour drawn for geometry with no explicit color() wherever a live
28
+ // theme cannot be consulted -- per-triangle colour arrays, which are baked.
29
+ // Matches the reference's SceneRenderer._default_color.
30
+ inline constexpr std::array<float, 4> kDefaultGeometryColor{0.9f, 0.85f, 0.1f, 1.0f};
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
+
26
39
  struct ColoredBody {
27
40
  std::optional<manifold::Manifold> body;
28
41
  std::optional<std::array<float, 4>> color; // RGBA; nullopt = "no explicit color() -- follow the live theme"
@@ -53,6 +66,16 @@ struct ColoredBody {
53
66
  BodyRole role = BodyRole::Normal;
54
67
  std::optional<std::vector<std::array<float, 4>>> triColors; // per-triangle RGBA, multi-color CSG merges only
55
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
+
56
79
  // Whether `body` is empty, once anything has asked (isEmptyBody,
57
80
  // csg_generate.cpp). Asking Manifold is not free: every accessor goes
58
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.2"
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
 
@@ -150,14 +151,6 @@ CSGParams resolveCsg(Evaluator& ev, const oscad::ModularCall& node, EvalContext&
150
151
 
151
152
  namespace {
152
153
 
153
- // Matches the reference's own SceneRenderer._default_color -- shown for
154
- // geometry with no explicit color() override. ColoredBody::color normally
155
- // stays nullopt for uncolored geometry so a live renderer can resolve it
156
- // against its own color theme, but a per-triangle triColors override has
157
- // no such live-resolution mechanism, so a concrete fallback is needed here
158
- // specifically.
159
- constexpr std::array<float, 4> kDefaultGeometryColor{0.9f, 0.85f, 0.1f, 1.0f};
160
-
161
154
  // After a real boolean merge, `cb.color` is just one arbitrary child's
162
155
  // color (the first contributing operand above) -- every other child's own
163
156
  // color is otherwise lost, e.g. union()-ing an opaque cube with a
@@ -207,6 +200,61 @@ void attachTriColors(Evaluator& ev, ColoredBody& cb) {
207
200
  cb.triColors = std::move(triColors);
208
201
  }
209
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
+
210
258
  // One colour's worth of 2D result.
211
259
  //
212
260
  // 3D recovers per-child colour AFTER the merge, from Manifold's own
@@ -294,6 +342,15 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
294
342
  // only produce a uniformly coloured result.
295
343
  std::optional<std::optional<std::array<float, 4>>> firstColor;
296
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;
297
354
 
298
355
  size_t stmtIndex = 0;
299
356
  for (const Value& sizeVal : groupSizes) {
@@ -346,6 +403,20 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
346
403
  if (!firstColor) firstColor = c.color;
347
404
  else if (*firstColor != c.color) mixedColors = true;
348
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
+ }
349
420
  for (const ColoredBody& c : split.foreground) {
350
421
  if (c.section) sections2d.push_back(c);
351
422
  }
@@ -385,8 +456,27 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
385
456
  cb.knownStatus = manifold::Manifold::Error::NoError; // every operand was
386
457
  cb.knownEmpty = grpEmpty;
387
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();
388
475
  } else if (op == "union") {
389
476
  res3d->body = *res3d->body + grp;
477
+ if (rememberParts) {
478
+ for (const ColoredBody& c : bodies3d) collectKeepParts(c, unionParts);
479
+ }
390
480
  if (known(res3d->knownEmpty, false) || known(grpEmpty, false)) res3d->knownEmpty = false;
391
481
  else if (known(res3d->knownEmpty, true) && known(grpEmpty, true)) res3d->knownEmpty = true;
392
482
  else res3d->knownEmpty.reset();
@@ -423,6 +513,14 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
423
513
  }
424
514
  }
425
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
+ }
426
524
  if (res3d && res3d->body && mixedColors) attachTriColors(ev, *res3d);
427
525
 
428
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
@@ -338,32 +363,89 @@ void Evaluator::restampCachedIds(std::vector<ColoredBody>& bodies, const oscad::
338
363
  manifold::MeshGL mesh = cb.body->GetMeshGL();
339
364
  if (mesh.runOriginalID.empty()) continue;
340
365
  std::unordered_map<uint32_t, uint32_t> remap;
341
- for (uint32_t& id : mesh.runOriginalID) {
366
+ // fresh ID -> the colour its triangles carried, multi-colour
367
+ // bodies only; what triColors is rebuilt from below.
368
+ std::unordered_map<uint32_t, std::array<float, 4>> runColor;
369
+ for (size_t run = 0; run < mesh.runOriginalID.size(); ++run) {
370
+ uint32_t& id = mesh.runOriginalID[run];
342
371
  auto found = remap.find(id);
343
372
  if (found == remap.end()) {
344
373
  const uint32_t fresh = manifold::Manifold::ReserveIDs(1);
345
374
  auto old = idToNode.find(id);
346
375
  idToNode[fresh] =
347
376
  (old != idToNode.end() && old->second != producer) ? old->second : &node;
377
+ // The colour has to be re-recorded too, or a later merge
378
+ // cannot tell this body's runs apart from uncoloured ones
379
+ // (attachTriColors looks runs up here). Across renders
380
+ // the old ID was minted by an earlier Evaluator and this
381
+ // one's idToColor has never heard of it, so the cached
382
+ // body itself is the record: its own colour, or for a
383
+ // multi-colour merge the colour its triangles carry. This
384
+ // is what made `difference() { color("orange") cube();
385
+ // color("cyan") cylinder(); ... }` lose the cyan on the
386
+ // first re-render after an edit and go flat orange on the
387
+ // next (BelfrySCAD #412) -- every cached operand looked
388
+ // uncoloured, so the merge saw nothing to distinguish.
348
389
  auto color = idToColor.find(id);
349
- if (color != idToColor.end()) idToColor[fresh] = color->second;
390
+ if (color != idToColor.end()) {
391
+ idToColor[fresh] = color->second;
392
+ } else if (cb.triColors && run + 1 < mesh.runIndex.size() &&
393
+ mesh.runIndex[run] / 3 < cb.triColors->size()) {
394
+ // ponytail: a run whose triangles carry the default
395
+ // fallback colour reads back as that explicit colour
396
+ // rather than "follow the theme"; the tint is the same.
397
+ idToColor[fresh] = (*cb.triColors)[mesh.runIndex[run] / 3];
398
+ } else {
399
+ idToColor[fresh] = cb.color;
400
+ }
401
+ if (cb.triColors && run + 1 < mesh.runIndex.size() &&
402
+ mesh.runIndex[run] / 3 < cb.triColors->size()) {
403
+ runColor[fresh] = (*cb.triColors)[mesh.runIndex[run] / 3];
404
+ }
350
405
  found = remap.emplace(id, fresh).first;
351
406
  }
352
407
  id = found->second;
353
408
  }
354
- const size_t trisBefore = mesh.triVerts.size() / 3;
355
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);
356
414
  // Rebuilding a Manifold from a mesh can clean it up (degenerate
357
- // triangles merged away), and triColors is indexed by triangle. An
358
- // array that no longer matches is worse than none: the renderer
359
- // masks its vertex arrays with it and raises IndexError rather
360
- // than drawing anything at all.
361
- if (cb.triColors && cb.body && cb.body->GetMeshGL().triVerts.size() / 3 != trisBefore) {
362
- cb.triColors.reset();
415
+ // triangles merged away), and triColors is indexed by triangle, so
416
+ // the old array no longer lines up. The RUNS survive the rebuild,
417
+ // and each run's colour was just recorded, so the array is
418
+ // rebuilt from them rather than dropped -- dropping it turned a
419
+ // fully cached multi-colour difference() flat (BelfrySCAD #412).
420
+ if (cb.triColors && cb.body) {
421
+ const manifold::MeshGL rebuilt = cb.body->GetMeshGL();
422
+ const size_t numTris = rebuilt.triVerts.size() / 3;
423
+ std::vector<std::array<float, 4>> triColors(numTris, cb.color.value_or(kDefaultGeometryColor));
424
+ for (size_t i = 0; i + 1 < rebuilt.runIndex.size() && i < rebuilt.runOriginalID.size(); ++i) {
425
+ auto it = runColor.find(rebuilt.runOriginalID[i]);
426
+ if (it == runColor.end()) continue;
427
+ const size_t start = rebuilt.runIndex[i] / 3;
428
+ const size_t end = std::min<size_t>(rebuilt.runIndex[i + 1] / 3, numTris);
429
+ for (size_t t = start; t < end; ++t) triColors[t] = it->second;
430
+ }
431
+ cb.triColors = std::move(triColors);
363
432
  }
364
433
  }
365
434
  }
366
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
+
367
449
  ColoredBody Evaluator::tagGenerated(manifold::Manifold body, const oscad::ASTNode& node, const Value& colorValue) {
368
450
  manifold::MeshGL mesh = body.GetMeshGL();
369
451
  std::optional<std::array<float, 4>> color = valueToColor(colorValue);
@@ -6,6 +6,7 @@
6
6
 
7
7
  #include <gtest/gtest.h>
8
8
  #include <algorithm>
9
+ #include <array>
9
10
  #include <iterator>
10
11
  #include <numbers>
11
12
  #include <set>
@@ -327,6 +328,46 @@ TEST(ManifoldCache, ACachedMultiPartSubtreeKeepsOneIdPerPart) {
327
328
  EXPECT_EQ(all.size(), 4u) << "parts of the reused subtree share IDs";
328
329
  }
329
330
 
331
+ namespace {
332
+ // Distinct RGB triples a body's triangles carry (its flat colour if it has
333
+ // no per-triangle colours).
334
+ std::set<std::array<float, 3>> distinctColors(const ColoredBody& cb) {
335
+ std::set<std::array<float, 3>> out;
336
+ if (cb.triColors) {
337
+ for (const auto& c : *cb.triColors) out.insert({c[0], c[1], c[2]});
338
+ } else if (cb.color) {
339
+ out.insert({(*cb.color)[0], (*cb.color)[1], (*cb.color)[2]});
340
+ }
341
+ return out;
342
+ }
343
+ } // namespace
344
+
345
+ TEST(ManifoldCache, CachedOperandsKeepTheirColoursInALaterMerge) {
346
+ // BelfrySCAD #412: edit one subtrahend's colour and re-render with the
347
+ // cache shared, as the GUI does. The unchanged operands are cache hits
348
+ // and their run colours were never re-recorded, so the merge saw them
349
+ // as uncoloured: first re-render lost the cyan, the next (every
350
+ // operand cached) collapsed to flat orange.
351
+ const std::string a =
352
+ "difference() { color(\"orange\") cube(20, center=true);"
353
+ " color(\"cyan\") cylinder(30, r=8, center=true, $fn=24);"
354
+ " color(\"magenta\") rotate([0,90,0]) cylinder(30, r=8, center=true, $fn=24); }";
355
+ const std::string b = [&] { std::string s = a; s.replace(s.find("magenta"), 7, "green"); return s; }();
356
+
357
+ const std::set<std::array<float, 3>> wantB = distinctColors(evalSrc(b).bodies[0]);
358
+ ASSERT_EQ(wantB.size(), 3u);
359
+
360
+ auto cache = std::make_shared<ManifoldCache>();
361
+ evalSrcWithCache(a, cache);
362
+ EXPECT_EQ(distinctColors(evalSrcWithCache(b, cache).bodies[0]), wantB) << "first re-render";
363
+ EXPECT_EQ(distinctColors(evalSrcWithCache(b, cache).bodies[0]), wantB) << "fully cached";
364
+ // And a cached multi-colour body merged again keeps every colour.
365
+ Evaluated wrapped = evalSrcWithCache("union() { " + b + " translate([30,0,0]) color(\"red\") cube(5); }", cache);
366
+ std::set<std::array<float, 3>> wantWrapped = wantB;
367
+ wantWrapped.insert({1.0f, 0.0f, 0.0f});
368
+ EXPECT_EQ(distinctColors(wrapped.bodies[0]), wantWrapped);
369
+ }
370
+
330
371
  TEST(ManifoldCache, OutputIsIdenticalCacheOnVsCacheOffAcrossBuiltinCategories) {
331
372
  const std::vector<std::string> scripts = {
332
373
  "cube(3);",