openscad-cpp-evaluator 1.14.0__tar.gz → 1.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/CLAUDE.md +16 -1
  2. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/bindings/module.cpp +9 -2
  4. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/export.hpp +8 -1
  5. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/pyproject.toml +1 -1
  6. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/python/openscad_cpp_evaluator/__init__.py +8 -1
  7. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/text.cpp +12 -1
  8. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/export.cpp +16 -4
  9. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_export.cpp +60 -0
  10. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_python_bindings.py +21 -0
  11. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_text.cpp +27 -0
  12. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/.github/workflows/ci.yml +0 -0
  13. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/.github/workflows/wheels.yml +0 -0
  14. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/.gitignore +0 -0
  15. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/.gitmodules +0 -0
  16. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/CMakeLists.txt +0 -0
  17. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/LICENSE +0 -0
  18. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/README.md +0 -0
  19. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/bindings/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/bindings/ast_to_py.cpp +0 -0
  21. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/cmake/embed_font.cmake +0 -0
  22. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/examples/CMakeLists.txt +0 -0
  23. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/examples/eval_perf_benchmark.cpp +0 -0
  24. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/examples/manifold_cache_reuse.cpp +0 -0
  25. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/examples/minimal_debugger.cpp +0 -0
  26. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  27. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/.gitignore +0 -0
  28. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  29. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  30. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/README.md +0 -0
  31. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  32. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  33. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  34. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  35. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  36. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  37. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  38. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  39. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  40. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  41. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  42. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  43. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  44. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  45. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  46. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  47. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  48. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  49. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  50. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  51. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  52. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  53. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  54. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  55. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  56. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  57. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  58. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  59. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  60. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  61. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  62. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  63. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  64. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  65. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  66. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  67. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  68. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  69. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  70. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  71. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  72. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  73. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  74. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  75. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  76. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  77. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  78. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  79. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  80. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  81. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  82. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  83. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  84. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  85. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  86. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  87. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  88. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  89. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  90. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  91. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  92. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  93. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  94. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  95. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  96. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  97. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  98. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  99. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  100. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  101. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  102. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  103. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  104. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  105. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  106. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  107. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  108. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  109. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  110. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  111. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  112. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  113. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  114. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  115. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  116. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  117. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  118. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  119. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  120. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  121. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  122. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  123. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  124. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  125. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  126. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  127. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  128. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  129. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/resources/fonts/LICENSE +0 -0
  130. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  131. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  132. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  133. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  134. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/CMakeLists.txt +0 -0
  135. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/booleans.cpp +0 -0
  136. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/builtins.hpp +0 -0
  137. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/call_args.cpp +0 -0
  138. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/color.cpp +0 -0
  139. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/control.cpp +0 -0
  140. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/dxf_dim.cpp +0 -0
  141. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/extrude.cpp +0 -0
  142. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/function_builtins.cpp +0 -0
  143. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/import.cpp +0 -0
  144. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/modifiers.cpp +0 -0
  145. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/primitives_2d.cpp +0 -0
  146. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/primitives_3d.cpp +0 -0
  147. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/registry.cpp +0 -0
  148. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/roof.cpp +0 -0
  149. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/segments.cpp +0 -0
  150. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/surface.cpp +0 -0
  151. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/topology.cpp +0 -0
  152. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/builtins/transforms.cpp +0 -0
  153. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/bytecode_compiler.cpp +0 -0
  154. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/bytecode_vm.cpp +0 -0
  155. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/colored_body.cpp +0 -0
  156. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/csg_generate.cpp +0 -0
  157. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/csg_resolve.cpp +0 -0
  158. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/css_colors.cpp +0 -0
  159. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/debug/debug_repl.cpp +0 -0
  160. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/debug_profile.cpp +0 -0
  161. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/eval_context.cpp +0 -0
  162. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/eval_error.cpp +0 -0
  163. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/eval_use.cpp +0 -0
  164. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/expr_eval.cpp +0 -0
  165. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/format_closure.cpp +0 -0
  166. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/import/dxf_import.cpp +0 -0
  167. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/import/mesh_import.cpp +0 -0
  168. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/import/surface_load.cpp +0 -0
  169. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/import/svg_import.cpp +0 -0
  170. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/manifold_cache.cpp +0 -0
  171. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/measure_geometry.cpp +0 -0
  172. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/mesh_check.cpp +0 -0
  173. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/stmt_eval.cpp +0 -0
  174. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/text/font_match.cpp +0 -0
  175. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/text/freetype_font_provider.cpp +0 -0
  176. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/text/text_metrics.cpp +0 -0
  177. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/user_calls.cpp +0 -0
  178. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/utf8.cpp +0 -0
  179. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/value.cpp +0 -0
  180. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/src/zip_stored.cpp +0 -0
  181. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/CMakeLists.txt +0 -0
  182. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_booleans.cpp +0 -0
  183. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_bytecode_compiler.cpp +0 -0
  184. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_children_separate.cpp +0 -0
  185. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_cli.cpp +0 -0
  186. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_control_flow.cpp +0 -0
  187. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_csg_tree.cpp +0 -0
  188. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_debug_hooks.cpp +0 -0
  189. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_dxf_svg_import.cpp +0 -0
  190. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_eval_context.cpp +0 -0
  191. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_expr_eval.cpp +0 -0
  192. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_extrude_roof.cpp +0 -0
  193. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_feature_detection.cpp +0 -0
  194. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_function_builtins.cpp +0 -0
  195. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_helpers.hpp +0 -0
  196. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_import_export.cpp +0 -0
  197. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_include_cache.cpp +0 -0
  198. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_manifold_cache.cpp +0 -0
  199. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_mesh_check.cpp +0 -0
  200. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_multi_color_merge.cpp +0 -0
  201. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_primitives.cpp +0 -0
  202. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_profiling.cpp +0 -0
  203. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_render_expr.cpp +0 -0
  204. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_scope_trail.cpp +0 -0
  205. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_scoping.cpp +0 -0
  206. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_surface.cpp +0 -0
  207. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_tail_calls.cpp +0 -0
  208. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_transforms.cpp +0 -0
  209. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_unexpected_args.cpp +0 -0
  210. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_use.cpp +0 -0
  211. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_value.cpp +0 -0
  212. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_viewport_params.cpp +0 -0
  213. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tests/test_zip_stored.cpp +0 -0
  214. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tools/cli/CMakeLists.txt +0 -0
  215. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tools/cli/cli_lib.cpp +0 -0
  216. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tools/cli/cli_lib.hpp +0 -0
  217. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/tools/cli/main.cpp +0 -0
  218. {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.15.0}/uv.lock +0 -0
@@ -298,6 +298,13 @@ variable real OpenSCAD reads). Family match is case-insensitive and required; st
298
298
  dropping to Regular beats falling through to an unrelated family, which is what fontconfig does
299
299
  too.
300
300
 
301
+ **`text()` takes `font` positionally too — and NOTHING after it.** `text(text, size, font)` is the
302
+ positional set; `halign`/`valign`/`spacing`/`direction`/`language`/`script` are name-only, because
303
+ that is what the 2026.02.01 binary does: `text("Hi", 10, "F", "center")` is not centred there while
304
+ `halign="center"` is, measured by bounding box. So **`text()` and `textmetrics()` genuinely differ**
305
+ — the latter takes all nine positionally — and neither can be assumed from the other. `font` being
306
+ name-only made `text("Hi", 10, "Liberation Sans:style=Bold")` silently draw the default face.
307
+
301
308
  **`font` is a POSITIONAL argument in both metrics functions**, and so is everything after it:
302
309
  `fontmetrics(size, font)` and `textmetrics(text, size, font, direction, language, script, halign,
303
310
  valign, spacing)`. Verified against the 2026.02.01 binary rather than read off a doc page. Having
@@ -970,7 +977,15 @@ grep for `ponytail:`.
970
977
  restating the list, because three hand-written copies had already drifted far enough that `.off`
971
978
  could be written by neither the GUI nor the CLI while every test passed.
972
979
  Colour is carried by 3MF, AMF, OBJ (companion `.mtl`), PLY, VRML and X3D; STL and OFF compose
973
- every body into one solid and store geometry only. **AMF** puts colour on the `<volume>` rather
980
+ every body into one solid and store geometry only.
981
+ **`ExportOptions::splitColors` picks what the file is FOR**, and is the axis worth asking a user
982
+ about rather than "should parts be separate": on (the default) writes one object per colour, which
983
+ is what a **multi-material** print wants — each colour is something the slicer assigns to a
984
+ filament. Off is a **single-material** print: everything unions into one welded solid, so touching
985
+ colour regions lose the coincident faces that stacking per-colour objects would leave, and the
986
+ per-triangle colour is dropped because such a print has no use for it. Implemented by forcing
987
+ `splitBodiesForExport`'s `allSame` branch, which already unions rather than concatenates —
988
+ concatenating the per-colour objects instead would have left the seam. **AMF** puts colour on the `<volume>` rather
974
989
  than the face, so an object whose triangles are not all one colour is written as one volume per
975
990
  colour — per-triangle `<color>` is legal in the spec but poorly supported by the slicers that are
976
991
  the audience for it.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.14.0
3
+ Version: 1.15.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -404,7 +404,8 @@ void applyPdfOptions(const nb::dict& d, oscadeval::ExportPdfOptions& pdf) {
404
404
  }
405
405
 
406
406
  nb::list exportModelPy(const std::string& path, const Geometry& geom, const std::string& format, bool asciiStl,
407
- bool stripSlivers, bool splitComponents, bool svgFill, const std::string& svgFillColor,
407
+ bool stripSlivers, bool splitComponents, bool splitColors, bool svgFill,
408
+ const std::string& svgFillColor,
408
409
  bool svgStroke, const std::string& svgStrokeColor, double svgStrokeWidth,
409
410
  nb::object pdfOptions) {
410
411
  oscadeval::ExportPdfOptions pdf;
@@ -418,6 +419,7 @@ nb::list exportModelPy(const std::string& path, const Geometry& geom, const std:
418
419
  opts.asciiStl = asciiStl;
419
420
  opts.stripSlivers = stripSlivers;
420
421
  opts.splitComponents = splitComponents;
422
+ opts.splitColors = splitColors;
421
423
  opts.svg.fill = svgFill;
422
424
  opts.svg.fillColor = svgFillColor;
423
425
  opts.svg.stroke = svgStroke;
@@ -850,7 +852,8 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
850
852
 
851
853
  m.def("export_model", &exportModelPy, nb::arg("path"), nb::arg("geometry"), nb::arg("format") = std::string(),
852
854
  nb::arg("ascii_stl") = false, nb::arg("strip_slivers") = true,
853
- nb::arg("split_components") = false, nb::arg("svg_fill") = false,
855
+ nb::arg("split_components") = false, nb::arg("split_colors") = true,
856
+ nb::arg("svg_fill") = false,
854
857
  nb::arg("svg_fill_color") = std::string("white"), nb::arg("svg_stroke") = true,
855
858
  nb::arg("svg_stroke_color") = std::string("black"), nb::arg("svg_stroke_width") = 0.35,
856
859
  nb::arg("pdf_options") = nb::none(),
@@ -861,6 +864,10 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
861
864
  "`split_components` gives every disconnected piece its own object in the multi-object formats "
862
865
  "(3MF, AMF, OBJ, PLY, VRML, X3D). Off by default, matching OpenSCAD, which writes one object "
863
866
  "per colour however many pieces it is in.\n\n"
867
+ "`split_colors` (on by default) writes one object per colour -- what a MULTI-MATERIAL print "
868
+ "wants, each colour being something the slicer assigns to a filament. Turn it off for a "
869
+ "single-material print: everything welds into one solid, with no seams between colour "
870
+ "regions and no per-triangle colour it has no use for.\n\n"
864
871
  "The svg_* arguments are OpenSCAD's -O export-svg/... set and apply to .svg only. "
865
872
  "svg_stroke/svg_stroke_width also pad the page, so they are not purely cosmetic. An .svg "
866
873
  "export needs an all-2D model and raises otherwise, as OpenSCAD does.\n\n"
@@ -53,7 +53,8 @@ struct ExportObject {
53
53
  // 1-based index is appended to `openParts` for the caller to warn about.
54
54
  std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& bodies,
55
55
  std::vector<int>* openParts = nullptr,
56
- bool splitComponents = false);
56
+ bool splitComponents = false,
57
+ bool splitColors = true);
57
58
 
58
59
  // Writes a binary STL: composes every body's mesh into one solid (bodies
59
60
  // with no `.body` -- 2D-only sections -- or an empty Manifold are
@@ -236,6 +237,12 @@ struct ExportOptions {
236
237
  // pieces it is in; see that function's own comment for why the default
237
238
  // moved here.
238
239
  bool splitComponents = false;
240
+ // Rule 2 of splitBodiesForExport: one object per colour. That is what
241
+ // a MULTI-MATERIAL print wants -- each colour becomes something the
242
+ // slicer assigns to a filament. Turn it off for a single-material
243
+ // print, which wants one welded solid instead: no seams between
244
+ // colour regions, and no per-triangle colour it has no use for.
245
+ bool splitColors = true;
239
246
  // .svg only.
240
247
  ExportSvgOptions svg;
241
248
  // .pdf only.
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.14.0"
7
+ version = "1.15.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -293,6 +293,7 @@ def strip_slivers(verts, tris):
293
293
 
294
294
  def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
295
295
  strip_slivers: bool = True, split_components: bool = False,
296
+ split_colors: bool = True,
296
297
  svg_fill: bool = False, svg_fill_color: str = "white",
297
298
  svg_stroke: bool = True, svg_stroke_color: str = "black",
298
299
  svg_stroke_width: float = 0.35, pdf_options: dict | None = None) -> list:
@@ -316,6 +317,12 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
316
317
  `split_components` gives every disconnected piece its own object in the
317
318
  multi-object formats. Off by default, which is what OpenSCAD writes.
318
319
 
320
+ `split_colors` (on by default) writes one object per colour -- what a
321
+ MULTI-MATERIAL print wants, each colour being something the slicer
322
+ assigns to a filament. Turn it off for a single-material print:
323
+ everything welds into one solid, with no seams between colour regions
324
+ and no per-triangle colour it has no use for.
325
+
319
326
  The `svg_*` arguments are OpenSCAD's `-O export-svg/...` set and apply
320
327
  to `.svg` only, which writes a 2D model at 1:1 in millimetres.
321
328
  `svg_stroke`/`svg_stroke_width` also pad the page, so they are not
@@ -335,7 +342,7 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
335
342
  """
336
343
  try:
337
344
  return list(_ext.export_model(path, geometry, format, ascii_stl, strip_slivers,
338
- split_components, svg_fill, svg_fill_color,
345
+ split_components, split_colors, svg_fill, svg_fill_color,
339
346
  svg_stroke, svg_stroke_color, svg_stroke_width,
340
347
  pdf_options))
341
348
  except Exception as e:
@@ -34,7 +34,18 @@ CSGParams resolveText(Evaluator& ev, const oscad::ModularCall& node, EvalContext
34
34
  auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
35
35
  const std::string text = asStringOr(getArg(args, 0, "text", Value{std::string("")}), "");
36
36
  const double size = toDoubleLenient(getArg(args, 1, "size", Value{10.0}));
37
- const std::string fontSpec = asStringOr(getArg(args, std::nullopt, "font", Value{std::string("")}), "");
37
+ // font is POSITIONAL, at index 2: `text("Hi", 10, "Liberation Sans")`
38
+ // is how people write it and how the reference reads it -- measured
39
+ // against the 2026.02.01 binary, whose bounding box for the
40
+ // positional form matches the named one exactly.
41
+ //
42
+ // Only font. halign/valign/spacing/direction/language/script stay
43
+ // name-only, because the same measurement shows the reference
44
+ // ignoring them positionally: `text("Hi", 10, "F", "center")` is NOT
45
+ // centred there, while halign="center" is. text() and textmetrics()
46
+ // genuinely differ here -- textmetrics takes all nine positionally --
47
+ // so neither can be assumed from the other.
48
+ const std::string fontSpec = asStringOr(getArg(args, 2, "font", Value{std::string("")}), "");
38
49
  const std::string halign = asStringOr(getArg(args, std::nullopt, "halign", Value{std::string("left")}), "left");
39
50
  const std::string valign = asStringOr(getArg(args, std::nullopt, "valign", Value{std::string("baseline")}), "baseline");
40
51
  const double spacing = toDoubleLenient(getArg(args, std::nullopt, "spacing", Value{1.0}));
@@ -421,7 +421,7 @@ std::vector<std::array<float, 4>> carryTriColors(const std::vector<std::array<fl
421
421
  } // namespace
422
422
 
423
423
  std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& bodies, std::vector<int>* openParts,
424
- bool splitComponents) {
424
+ bool splitComponents, bool splitColors) {
425
425
  struct Solid {
426
426
  manifold::Manifold man;
427
427
  std::optional<std::array<float, 4>> color;
@@ -484,7 +484,15 @@ std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& b
484
484
  if (k.hasColor) k.color = *solids[i].color;
485
485
  keys.push_back(k);
486
486
  }
487
- const bool allSame = std::all_of(keys.begin(), keys.end(), [&](const ColorKey& k) { return k == keys[0]; });
487
+ // Splitting by colour is for a MULTI-MATERIAL print, where each
488
+ // colour becomes an object the slicer assigns to a filament. A
489
+ // single-material print wants the opposite: one welded solid, with
490
+ // no seams between colour regions for the slicer to treat as
491
+ // separate walls. Unioning everything here (rather than
492
+ // concatenating the per-colour objects afterwards) is what makes
493
+ // touching regions weld instead of leaving coincident faces.
494
+ const bool allSame = !splitColors ||
495
+ std::all_of(keys.begin(), keys.end(), [&](const ColorKey& k) { return k == keys[0]; });
488
496
 
489
497
  if (allSame) {
490
498
  // The common case by far, and it needs no per-body subtraction
@@ -496,7 +504,10 @@ std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& b
496
504
  Claimed c;
497
505
  c.man = addAll(parts);
498
506
  c.color = solids[0].color;
499
- c.triColors = solids[0].triColors;
507
+ // Per-triangle colour cannot survive a weld across colours --
508
+ // the union rebuilds the surface -- and a single-material
509
+ // print has no use for it anyway.
510
+ c.triColors = splitColors ? solids[0].triColors : std::vector<std::array<float, 4>>{};
500
511
  c.sourceTris = solids[0].tris;
501
512
  c.key = keys[0];
502
513
  claimedGroups.push_back(std::move(c));
@@ -1507,7 +1518,8 @@ std::vector<std::string> exportModel(const std::string& path, const std::vector<
1507
1518
  // its own -- that is what the file contains.
1508
1519
  for (std::string& w : checkExportBodies(bodies)) warnings.push_back(std::move(w));
1509
1520
  std::vector<int> openParts;
1510
- const std::vector<ExportObject> objects = splitBodiesForExport(bodies, &openParts, opts.splitComponents);
1521
+ const std::vector<ExportObject> objects =
1522
+ splitBodiesForExport(bodies, &openParts, opts.splitComponents, opts.splitColors);
1511
1523
  reportOpen(openParts);
1512
1524
  if (objects.empty()) throw std::runtime_error("No geometry to export");
1513
1525
  if (ext == ".3mf") {
@@ -733,3 +733,63 @@ TEST(ExportPdf, ReachableThroughExportModelAndListedAsAnExtension) {
733
733
  std::remove(path.c_str());
734
734
  }
735
735
 
736
+
737
+ // -- single-material vs multi-material -------------------------------------
738
+
739
+ TEST(SplitColors, MultiMaterialGivesOneObjectPerColour) {
740
+ // The default: each colour becomes something a slicer can assign to a
741
+ // filament.
742
+ std::vector<ColoredBody> bodies =
743
+ evalToBodies("color(\"red\") cube(10); color(\"blue\") translate([12,0,0]) cube(10);");
744
+ const std::vector<ExportObject> objs = splitBodiesForExport(bodies, nullptr, false, /*splitColors=*/true);
745
+ EXPECT_EQ(objs.size(), 2u);
746
+ }
747
+
748
+ TEST(SplitColors, SingleMaterialGivesOneObject) {
749
+ // A single-material print has nothing to do with the colours, and
750
+ // splitting on them only gives the slicer parts to list.
751
+ std::vector<ColoredBody> bodies =
752
+ evalToBodies("color(\"red\") cube(10); color(\"blue\") translate([12,0,0]) cube(10);");
753
+ const std::vector<ExportObject> objs = splitBodiesForExport(bodies, nullptr, false, /*splitColors=*/false);
754
+ ASSERT_EQ(objs.size(), 1u);
755
+ EXPECT_TRUE(objs[0].triColors.empty()); // no per-triangle colour to carry
756
+ }
757
+
758
+ TEST(SplitColors, SingleMaterialWeldsTouchingColourRegions) {
759
+ // Unioned, not concatenated: two touching cubes of different colours
760
+ // must come out as one solid without the coincident interior faces
761
+ // that stacking the per-colour objects would leave behind.
762
+ std::vector<ColoredBody> bodies =
763
+ evalToBodies("color(\"red\") cube(10); color(\"blue\") translate([10,0,0]) cube(10);");
764
+ const std::vector<ExportObject> single = splitBodiesForExport(bodies, nullptr, false, /*splitColors=*/false);
765
+ ASSERT_EQ(single.size(), 1u);
766
+
767
+ const std::vector<ExportObject> multi = splitBodiesForExport(bodies, nullptr, false, true);
768
+ ASSERT_EQ(multi.size(), 2u);
769
+ size_t multiTris = 0;
770
+ for (const ExportObject& o : multi) multiTris += o.tris.size() / 3;
771
+
772
+ // Fewer triangles than the two objects have between them: the pair of
773
+ // coincident faces along the seam is gone, which is the difference
774
+ // between a welded solid and two boxes stacked in one object. (Not
775
+ // the 12 of a plain 20x10x10 box -- Manifold keeps the seam's
776
+ // vertices, so the faces crossing it stay split.)
777
+ EXPECT_LT(single[0].tris.size() / 3, multiTris);
778
+ EXPECT_EQ(single[0].tris.size() / 3, 20u);
779
+ }
780
+
781
+ TEST(SplitColors, SingleMaterialKeepsTheWholeVolume) {
782
+ std::vector<ColoredBody> bodies =
783
+ evalToBodies("color(\"red\") cube(10); color(\"blue\") translate([12,0,0]) cube(10);");
784
+ const std::vector<ExportObject> multi = splitBodiesForExport(bodies, nullptr, false, true);
785
+ const std::vector<ExportObject> single = splitBodiesForExport(bodies, nullptr, false, false);
786
+ size_t multiTris = 0;
787
+ for (const ExportObject& o : multi) multiTris += o.tris.size();
788
+ EXPECT_EQ(single[0].tris.size(), multiTris); // same surface, one object
789
+ }
790
+
791
+ TEST(SplitColors, AnUncolouredModelIsUnaffectedEitherWay) {
792
+ std::vector<ColoredBody> bodies = evalToBodies("cube(10); translate([12,0,0]) cube(10);");
793
+ EXPECT_EQ(splitBodiesForExport(bodies, nullptr, false, true).size(), 1u);
794
+ EXPECT_EQ(splitBodiesForExport(bodies, nullptr, false, false).size(), 1u);
795
+ }
@@ -776,6 +776,27 @@ def test_list_fonts_reports_specs_that_actually_resolve(tmp_path):
776
776
  assert echoes == ['ECHO: "Regular"', 'ECHO: "Bold"', 'ECHO: "Italic"', 'ECHO: "Bold Italic"']
777
777
 
778
778
 
779
+ def test_split_colors_chooses_single_or_multi_material(tmp_path):
780
+ """Same two-hand-written-parameter-lists trap as split_components."""
781
+ from openscad_cpp_evaluator import Evaluator, export_model
782
+
783
+ src = tmp_path / "two.scad"
784
+ src.write_text('color("red") cube(10); color("blue") translate([12,0,0]) cube(10);')
785
+ ev = Evaluator()
786
+ ev.evaluate(str(src), {})
787
+
788
+ def objects_in(path, **kwargs):
789
+ import zipfile
790
+ export_model(str(path), ev.geometry, **kwargs)
791
+ with zipfile.ZipFile(path) as z:
792
+ return z.read("3D/3dmodel.model").decode().count("<object ")
793
+
794
+ # Multi-material (the default): one object per colour for the slicer.
795
+ assert objects_in(tmp_path / "multi.3mf") == 2
796
+ # Single-material: one solid, colours irrelevant.
797
+ assert objects_in(tmp_path / "single.3mf", split_colors=False) == 1
798
+
799
+
779
800
  def test_strict_commas_reaches_the_parser(tmp_path):
780
801
  """Evaluator.evaluate(strict_commas=True) must survive both hand-written
781
802
  parameter lists -- the facade's and the nanobind binding's. The last
@@ -379,3 +379,30 @@ TEST(FontArgs, TheLaterPositionalsLineUpToo) {
379
379
  ASSERT_EQ(echoes.size(), 1u);
380
380
  EXPECT_EQ(echoes[0], "ECHO: true, true");
381
381
  }
382
+
383
+ TEST(TextArgs, FontIsPositionalButTheRestAreNot) {
384
+ // text("Hi", 10, "Liberation Sans:style=Bold") silently drew the
385
+ // DEFAULT face: `font` was name-only, so the third argument went
386
+ // nowhere. Same defect as BelfrySCAD #381's, in the module people
387
+ // actually use.
388
+ //
389
+ // And only `font`: the reference accepts nothing after it
390
+ // positionally, which was measured rather than assumed --
391
+ // `text("Hi", 10, "F", "center")` is not centred in OpenSCAD
392
+ // 2026.02.01, while halign="center" is. textmetrics() differs from
393
+ // text() here, taking all nine positionally.
394
+ std::vector<std::string> echoes;
395
+ Evaluated e = evalSrc(
396
+ "p = render() { linear_extrude(1) text(\"Hi\", 10, \"Liberation Sans:style=Bold\"); };\n"
397
+ "n = render() { linear_extrude(1) text(\"Hi\", 10, font=\"Liberation Sans:style=Bold\"); };\n"
398
+ "d = render() { linear_extrude(1) text(\"Hi\", 10); };\n"
399
+ "h = render() { linear_extrude(1) text(\"Hi\", 10, \"Liberation Sans\", \"center\"); };\n"
400
+ "hn = render() { linear_extrude(1) text(\"Hi\", 10, \"Liberation Sans\", halign=\"center\"); };\n"
401
+ "echo(p.volume == n.volume, p.volume == d.volume, h.boundingbox == d.boundingbox,"
402
+ " hn.boundingbox == d.boundingbox);\n",
403
+ [&](const std::string& m) { echoes.push_back(m); });
404
+ ASSERT_EQ(echoes.size(), 1u);
405
+ // positional == named, positional != default, a 4th positional does
406
+ // NOTHING, and halign by name does move it.
407
+ EXPECT_EQ(echoes[0], "ECHO: true, false, true, false");
408
+ }