openscad-cpp-evaluator 1.7.0__tar.gz → 1.8.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 (214) hide show
  1. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/PKG-INFO +1 -1
  2. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/bindings/module.cpp +7 -2
  3. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/bytecode.hpp +9 -0
  4. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/export.hpp +16 -2
  5. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/pyproject.toml +1 -1
  6. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/python/openscad_cpp_evaluator/__init__.py +7 -3
  7. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/bytecode_compiler.cpp +24 -11
  8. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/bytecode_vm.cpp +11 -0
  9. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/export.cpp +9 -5
  10. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/mesh_check.cpp +99 -28
  11. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_bytecode_compiler.cpp +32 -0
  12. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_export.cpp +51 -1
  13. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_mesh_check.cpp +27 -0
  14. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_python_bindings.py +26 -0
  15. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/.github/workflows/ci.yml +0 -0
  16. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/.github/workflows/wheels.yml +0 -0
  17. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/.gitignore +0 -0
  18. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/.gitmodules +0 -0
  19. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/CLAUDE.md +0 -0
  20. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/CMakeLists.txt +0 -0
  21. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/LICENSE +0 -0
  22. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/README.md +0 -0
  23. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/bindings/CMakeLists.txt +0 -0
  24. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/bindings/ast_to_py.cpp +0 -0
  25. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/cmake/embed_font.cmake +0 -0
  26. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/examples/CMakeLists.txt +0 -0
  27. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/examples/eval_perf_benchmark.cpp +0 -0
  28. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/examples/manifold_cache_reuse.cpp +0 -0
  29. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/examples/minimal_debugger.cpp +0 -0
  30. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  31. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/.gitignore +0 -0
  32. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  33. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  34. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/README.md +0 -0
  35. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  36. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  37. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  38. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  39. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  40. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  41. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  42. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  43. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  44. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  45. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  46. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  47. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  48. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  49. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  50. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  51. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  52. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  53. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  54. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  55. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  56. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  57. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  58. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  59. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  60. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  61. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  62. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  63. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  64. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  65. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  66. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  67. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  68. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  69. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  70. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  71. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  72. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  73. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  74. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  75. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  76. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  77. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  78. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  79. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  80. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  81. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  82. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  83. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  84. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  85. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  86. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  87. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  88. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  89. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  90. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  91. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  92. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  93. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  94. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  95. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  96. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  97. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  98. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  99. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  100. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  101. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  102. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  103. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  104. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  105. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  106. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  107. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  108. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  109. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  110. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  111. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  112. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  113. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  114. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  115. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  116. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  117. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  118. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  119. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  120. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  121. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  122. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  123. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  124. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  125. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  126. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  127. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  128. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  129. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  130. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  131. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/resources/fonts/LICENSE +0 -0
  132. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  133. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/CMakeLists.txt +0 -0
  134. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/booleans.cpp +0 -0
  135. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/builtins.hpp +0 -0
  136. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/call_args.cpp +0 -0
  137. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/color.cpp +0 -0
  138. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/control.cpp +0 -0
  139. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/dxf_dim.cpp +0 -0
  140. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/extrude.cpp +0 -0
  141. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/function_builtins.cpp +0 -0
  142. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/import.cpp +0 -0
  143. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/modifiers.cpp +0 -0
  144. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/primitives_2d.cpp +0 -0
  145. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/primitives_3d.cpp +0 -0
  146. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/registry.cpp +0 -0
  147. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/roof.cpp +0 -0
  148. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/segments.cpp +0 -0
  149. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/surface.cpp +0 -0
  150. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/text.cpp +0 -0
  151. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/topology.cpp +0 -0
  152. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/builtins/transforms.cpp +0 -0
  153. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/colored_body.cpp +0 -0
  154. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/csg_generate.cpp +0 -0
  155. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/csg_resolve.cpp +0 -0
  156. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/css_colors.cpp +0 -0
  157. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/debug/debug_repl.cpp +0 -0
  158. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/debug_profile.cpp +0 -0
  159. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/eval_context.cpp +0 -0
  160. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/eval_error.cpp +0 -0
  161. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/eval_use.cpp +0 -0
  162. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/expr_eval.cpp +0 -0
  163. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/format_closure.cpp +0 -0
  164. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/import/dxf_import.cpp +0 -0
  165. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/import/mesh_import.cpp +0 -0
  166. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/import/surface_load.cpp +0 -0
  167. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/import/svg_import.cpp +0 -0
  168. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/manifold_cache.cpp +0 -0
  169. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/measure_geometry.cpp +0 -0
  170. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/stmt_eval.cpp +0 -0
  171. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/text/font_match.cpp +0 -0
  172. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/text/freetype_font_provider.cpp +0 -0
  173. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/text/text_metrics.cpp +0 -0
  174. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/user_calls.cpp +0 -0
  175. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/utf8.cpp +0 -0
  176. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/value.cpp +0 -0
  177. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/src/zip_stored.cpp +0 -0
  178. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/CMakeLists.txt +0 -0
  179. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_booleans.cpp +0 -0
  180. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_children_separate.cpp +0 -0
  181. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_cli.cpp +0 -0
  182. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_control_flow.cpp +0 -0
  183. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_csg_tree.cpp +0 -0
  184. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_debug_hooks.cpp +0 -0
  185. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_dxf_svg_import.cpp +0 -0
  186. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_eval_context.cpp +0 -0
  187. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_expr_eval.cpp +0 -0
  188. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_extrude_roof.cpp +0 -0
  189. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_feature_detection.cpp +0 -0
  190. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_function_builtins.cpp +0 -0
  191. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_helpers.hpp +0 -0
  192. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_import_export.cpp +0 -0
  193. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_include_cache.cpp +0 -0
  194. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_manifold_cache.cpp +0 -0
  195. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_multi_color_merge.cpp +0 -0
  196. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_primitives.cpp +0 -0
  197. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_profiling.cpp +0 -0
  198. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_render_expr.cpp +0 -0
  199. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_scope_trail.cpp +0 -0
  200. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_scoping.cpp +0 -0
  201. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_surface.cpp +0 -0
  202. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_tail_calls.cpp +0 -0
  203. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_text.cpp +0 -0
  204. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_transforms.cpp +0 -0
  205. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_unexpected_args.cpp +0 -0
  206. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_use.cpp +0 -0
  207. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_value.cpp +0 -0
  208. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_viewport_params.cpp +0 -0
  209. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tests/test_zip_stored.cpp +0 -0
  210. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tools/cli/CMakeLists.txt +0 -0
  211. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tools/cli/cli_lib.cpp +0 -0
  212. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tools/cli/cli_lib.hpp +0 -0
  213. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/tools/cli/main.cpp +0 -0
  214. {openscad_cpp_evaluator-1.7.0 → openscad_cpp_evaluator-1.8.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -357,7 +357,7 @@ struct Geometry {
357
357
  // exportModel, with the path/format/warnings marshalling. Releases the GIL:
358
358
  // a large export is seconds of Manifold work with no Python involved.
359
359
  nb::list exportModelPy(const std::string& path, const Geometry& geom, const std::string& format, bool asciiStl,
360
- bool stripSlivers) {
360
+ bool stripSlivers, bool splitComponents) {
361
361
  std::vector<std::string> warnings;
362
362
  {
363
363
  nb::gil_scoped_release rel;
@@ -365,6 +365,7 @@ nb::list exportModelPy(const std::string& path, const Geometry& geom, const std:
365
365
  opts.format = format;
366
366
  opts.asciiStl = asciiStl;
367
367
  opts.stripSlivers = stripSlivers;
368
+ opts.splitComponents = splitComponents;
368
369
  warnings = oscadeval::exportModel(path, geom.bodies, opts);
369
370
  }
370
371
  nb::list out;
@@ -786,10 +787,14 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
786
787
 
787
788
  m.def("export_model", &exportModelPy, nb::arg("path"), nb::arg("geometry"), nb::arg("format") = std::string(),
788
789
  nb::arg("ascii_stl") = false, nb::arg("strip_slivers") = true,
790
+ nb::arg("split_components") = false,
789
791
  "Write `geometry` to `path`, format taken from the extension unless `format` says otherwise. "
790
792
  "Returns the warnings to surface (open shells, mesh problems, slivers removed) rather than "
791
793
  "logging them. Raises RuntimeError when there is no geometry, the format is unknown, or the "
792
- "file cannot be opened.");
794
+ "file cannot be opened.\n\n"
795
+ "`split_components` gives every disconnected piece its own object in the multi-object formats "
796
+ "(3MF, AMF, OBJ, PLY, VRML, X3D). Off by default, matching OpenSCAD, which writes one object "
797
+ "per colour however many pieces it is in.");
793
798
 
794
799
  m.def("export_extensions", []() { return oscadeval::exportExtensions(); },
795
800
  "The file extensions export_model understands, dot-prefixed. Read from the C++ writer table "
@@ -816,6 +816,15 @@ struct CompiledChunk {
816
816
  // and copying it back out.
817
817
  struct ArgBind {
818
818
  int paramIndex = -1; // >=0: this argument binds that parameter
819
+ // Any FURTHER parameters of the same name. Normally empty, but a
820
+ // declaration may repeat a parameter name -- BOSL2's
821
+ // regular_prism() declares both `length` and `height` twice --
822
+ // and the name-matching path this replaced bound every one of
823
+ // them, because it walked the parameter list asking "is there a
824
+ // bound argument called this?" rather than the other way round.
825
+ // Binding only the first silently left the body reading undef
826
+ // from whichever slot the duplicate name finally resolved to.
827
+ std::vector<int> alsoParams;
819
828
  bool toDyn = false; // instead bind by name into ctx.dyn ($-named, undeclared)
820
829
  bool warnUnexpectedNamed = false;
821
830
  bool warnTooManyPositional = false;
@@ -39,13 +39,21 @@ struct ExportObject {
39
39
  // `color("red") body(); color("blue") detail();` leaves the detail
40
40
  // whole. Only the invisible interior is affected: the visible surface
41
41
  // is identical either way.
42
- // 3. One object per connected component.
42
+ // 3. One object per connected component -- OPTIONAL, and off by default.
43
+ // Rules 1 and 2 are about correctness; this one is presentation, and
44
+ // it is the one that diverges from OpenSCAD, which writes a single
45
+ // object however many disjoint pieces the model has. Splitting turned
46
+ // `cube(10); translate([20,0,0]) cube(10);` into two 3MF objects and a
47
+ // multiboard tile into hundreds, which slicers list individually and
48
+ // Prusa Slicer complains about (issue #319). Ask for it when separate
49
+ // pieces are what you want; leave it off to match OpenSCAD.
43
50
  //
44
51
  // A body Manifold rejected (an open shell is not a solid) can take part in
45
52
  // none of that: it keeps its own triangles and its own object, and its
46
53
  // 1-based index is appended to `openParts` for the caller to warn about.
47
54
  std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& bodies,
48
- std::vector<int>* openParts = nullptr);
55
+ std::vector<int>* openParts = nullptr,
56
+ bool splitComponents = false);
49
57
 
50
58
  // Writes a binary STL: composes every body's mesh into one solid (bodies
51
59
  // with no `.body` -- 2D-only sections -- or an empty Manifold are
@@ -130,6 +138,12 @@ struct ExportOptions {
130
138
  // break no topology, but slicers commonly discard them and are then
131
139
  // left with the holes their removal opens.
132
140
  bool stripSlivers = true;
141
+ // Rule 3 of splitBodiesForExport: give every disconnected piece its own
142
+ // object in the multi-object formats (3MF, AMF, OBJ, PLY, VRML, X3D).
143
+ // Off matches OpenSCAD, which writes one object per colour however many
144
+ // pieces it is in; see that function's own comment for why the default
145
+ // moved here.
146
+ bool splitComponents = false;
133
147
  };
134
148
 
135
149
  // Writes `bodies` to `path`, choosing the writer from the extension (or
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.7.0"
7
+ version = "1.8.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -292,13 +292,13 @@ def strip_slivers(verts, tris):
292
292
 
293
293
 
294
294
  def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
295
- strip_slivers: bool = True) -> list:
295
+ strip_slivers: bool = True, split_components: bool = False) -> list:
296
296
  """Write `geometry` to `path`. Returns the warnings to surface.
297
297
 
298
298
  `geometry` is the handle an Evaluator stashes on itself as
299
299
  `.geometry` -- the evaluated bodies, still on the C++ side. Export has
300
300
  to do real CSG (the implicit top-level union, the per-colour volume
301
- claim, the connected-component split), and going through the flattened
301
+ claim), and going through the flattened
302
302
  arrays the renderer gets would mean rebuilding every Manifold first:
303
303
  ~146ms on a 224k-triangle model, and Manifold's own provenance lost
304
304
  along the way.
@@ -309,9 +309,13 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
309
309
  here refuses to write: a deliberately open surface is a legitimate
310
310
  export. Raises EvalError when there is no geometry, the format is
311
311
  unknown, or the file cannot be opened.
312
+
313
+ `split_components` gives every disconnected piece its own object in the
314
+ multi-object formats. Off by default, which is what OpenSCAD writes.
312
315
  """
313
316
  try:
314
- return list(_ext.export_model(path, geometry, format, ascii_stl, strip_slivers))
317
+ return list(_ext.export_model(path, geometry, format, ascii_stl, strip_slivers,
318
+ split_components))
315
319
  except Exception as e:
316
320
  raise EvalError(str(e)) from e
317
321
 
@@ -20,6 +20,22 @@
20
20
  namespace oscadeval {
21
21
 
22
22
  namespace {
23
+ // Records every parameter called `name`, not just the first. See
24
+ // CompiledChunk::CallSite::ArgBind::alsoParams for why there can be more
25
+ // than one.
26
+ void bindEveryParamNamed(CompiledChunk::CallSite::ArgBind& b,
27
+ const std::vector<std::unique_ptr<oscad::ParameterDeclaration>>& params,
28
+ const std::string& name) {
29
+ for (size_t p = 0; p < params.size(); ++p) {
30
+ if (params[p]->name->name != name) continue;
31
+ if (b.paramIndex < 0) {
32
+ b.paramIndex = static_cast<int>(p);
33
+ } else {
34
+ b.alsoParams.push_back(static_cast<int>(p));
35
+ }
36
+ }
37
+ }
38
+
23
39
  // Replays buildBoundArgs' own positional/named matching rule (bytecode_vm.cpp)
24
40
  // against a parameter list known at compile time, recording the answer per
25
41
  // argument instead of recomputing it per call. The rule, unchanged: a named
@@ -38,19 +54,16 @@ void planCallSiteArgs(CompiledChunk::CallSite& site,
38
54
  CompiledChunk::CallSite::ArgBind& b = site.argBinds[i];
39
55
  if (site.argNames[i]) {
40
56
  const std::string& name = *site.argNames[i];
41
- int found = -1;
42
- for (size_t p = 0; p < nparams; ++p) {
43
- if (params[p]->name->name == name) {
44
- found = static_cast<int>(p);
45
- break;
46
- }
47
- }
48
- b.paramIndex = found;
49
- b.warnUnexpectedNamed = (found < 0) && !isConfigVariable(name);
50
- b.toDyn = (found < 0) && !name.empty() && name[0] == '$';
57
+ bindEveryParamNamed(b, params, name);
58
+ const bool found = b.paramIndex >= 0;
59
+ b.warnUnexpectedNamed = !found && !isConfigVariable(name);
60
+ b.toDyn = !found && !name.empty() && name[0] == '$';
51
61
  } else {
52
62
  if (positionalIdx < nparams) {
53
- b.paramIndex = static_cast<int>(positionalIdx);
63
+ // By NAME, not just by index: a positional argument landing on
64
+ // a repeated name filled every parameter of that name too,
65
+ // because the old path keyed its bound arguments by name.
66
+ bindEveryParamNamed(b, params, params[positionalIdx]->name->name);
54
67
  } else if (positionalIdx == nparams) {
55
68
  b.warnTooManyPositional = true;
56
69
  }
@@ -151,6 +151,17 @@ void bindPlannedArgsIntoFrame(const CompiledChunk& chunk, const CompiledChunk::C
151
151
  for (size_t i = 0; i < argCount; ++i) {
152
152
  const CompiledChunk::CallSite::ArgBind& b = site.argBinds[i];
153
153
  if (b.paramIndex >= 0) {
154
+ // The duplicate-name case (alsoParams) needs the value more than
155
+ // once, so it is copied there and only the last one moves.
156
+ for (const int extra : b.alsoParams) {
157
+ const CompiledChunk::Param& e = chunk.params[static_cast<size_t>(extra)];
158
+ if (e.isDyn) {
159
+ ctx.dyn->set(e.name, args[i]);
160
+ } else {
161
+ frame.slots[static_cast<size_t>(e.slot)] = args[i];
162
+ }
163
+ frame.bound[static_cast<size_t>(extra)] = true;
164
+ }
154
165
  const CompiledChunk::Param& p = chunk.params[static_cast<size_t>(b.paramIndex)];
155
166
  if (p.isDyn) {
156
167
  ctx.dyn->set(p.name, std::move(args[i]));
@@ -418,7 +418,8 @@ std::vector<std::array<float, 4>> carryTriColors(const std::vector<std::array<fl
418
418
 
419
419
  } // namespace
420
420
 
421
- std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& bodies, std::vector<int>* openParts) {
421
+ std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& bodies, std::vector<int>* openParts,
422
+ bool splitComponents) {
422
423
  struct Solid {
423
424
  manifold::Manifold man;
424
425
  std::optional<std::array<float, 4>> color;
@@ -545,9 +546,12 @@ std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& b
545
546
 
546
547
  std::vector<ExportObject> out;
547
548
  for (const Claimed& g : claimedGroups) {
548
- // Decompose() is the rule-3 split. A single-component solid comes
549
- // back as a one-element list, so there is no special case here.
550
- std::vector<manifold::Manifold> parts = g.man.Decompose();
549
+ // Decompose() is the rule-3 split, and it is opt-in: without it this
550
+ // colour group stays one object however many disjoint pieces it is
551
+ // in, which is what OpenSCAD writes. A single-component solid comes
552
+ // back as a one-element list either way, so there is no special case.
553
+ std::vector<manifold::Manifold> parts;
554
+ if (splitComponents) parts = g.man.Decompose();
551
555
  if (parts.empty()) parts.push_back(g.man);
552
556
  for (const manifold::Manifold& part : parts) {
553
557
  if (part.IsEmpty()) continue;
@@ -1022,7 +1026,7 @@ std::vector<std::string> exportModel(const std::string& path, const std::vector<
1022
1026
  // its own -- that is what the file contains.
1023
1027
  for (std::string& w : checkExportBodies(bodies)) warnings.push_back(std::move(w));
1024
1028
  std::vector<int> openParts;
1025
- const std::vector<ExportObject> objects = splitBodiesForExport(bodies, &openParts);
1029
+ const std::vector<ExportObject> objects = splitBodiesForExport(bodies, &openParts, opts.splitComponents);
1026
1030
  reportOpen(openParts);
1027
1031
  if (objects.empty()) throw std::runtime_error("No geometry to export");
1028
1032
  if (ext == ".3mf") {
@@ -257,16 +257,40 @@ template <typename M>
257
257
  std::vector<Vert> weldMap(const M& m, size_t& welded) {
258
258
  const size_t stride = m.numProp ? m.numProp : 3;
259
259
  const size_t n = stride ? m.vertProperties.size() / stride : 0;
260
- std::map<std::tuple<long long, long long, long long>, Vert> first;
261
260
  std::vector<Vert> remap(n);
262
261
  welded = 0;
262
+
263
+ // Grid key plus the vertex it came from, sorted so that equal positions
264
+ // land in a run. This was a std::map keyed by the tuple -- one tree node
265
+ // per VERTEX, ~885,000 of them for a 295K-triangle import -- and sorting
266
+ // gets the same answer with one allocation. Sorting by (key, vertex)
267
+ // puts the LOWEST-numbered vertex first in its run, which is the one
268
+ // emplace() kept as the representative, so the remap is unchanged.
269
+ struct Keyed {
270
+ long long x, y, z;
271
+ Vert v;
272
+ bool operator<(const Keyed& o) const {
273
+ if (x != o.x) return x < o.x;
274
+ if (y != o.y) return y < o.y;
275
+ if (z != o.z) return z < o.z;
276
+ return v < o.v;
277
+ }
278
+ bool samePos(const Keyed& o) const { return x == o.x && y == o.y && z == o.z; }
279
+ };
280
+ std::vector<Keyed> keyed;
281
+ keyed.reserve(n);
263
282
  for (size_t v = 0; v < n; ++v) {
264
283
  double p[3];
265
284
  pos(m, static_cast<Vert>(v), p);
266
- auto key = std::make_tuple(llround(p[0] * 1e6), llround(p[1] * 1e6), llround(p[2] * 1e6));
267
- auto [it, fresh] = first.emplace(key, static_cast<Vert>(v));
268
- remap[v] = it->second;
269
- if (!fresh) ++welded;
285
+ keyed.push_back({llround(p[0] * 1e6), llround(p[1] * 1e6), llround(p[2] * 1e6), static_cast<Vert>(v)});
286
+ }
287
+ std::sort(keyed.begin(), keyed.end());
288
+ for (size_t i = 0; i < keyed.size();) {
289
+ size_t j = i + 1;
290
+ while (j < keyed.size() && keyed[j].samePos(keyed[i])) ++j;
291
+ for (size_t k = i; k < j; ++k) remap[keyed[k].v] = keyed[i].v;
292
+ welded += j - i - 1;
293
+ i = j;
270
294
  }
271
295
  return remap;
272
296
  }
@@ -274,17 +298,28 @@ std::vector<Vert> weldMap(const M& m, size_t& welded) {
274
298
  // Boundary loops, as ordered vertex rings. Each boundary edge belongs to
275
299
  // exactly one, so following them from any start returns to it.
276
300
  std::vector<std::vector<Vert>> boundaryLoops(const std::vector<std::array<Vert, 3>>& tris) {
277
- std::map<Edge, int> count;
278
- std::multimap<Vert, Vert> next; // directed, along the boundary
301
+ // Every edge, sorted, so "used exactly once" is a run of length one --
302
+ // the same answer the std::map<Edge,int> gave, without a tree node per
303
+ // edge. `next` stays a multimap: boundary edges are a handful even on a
304
+ // badly broken mesh, and it is erased from while being traversed.
305
+ std::vector<uint64_t> edges;
306
+ edges.reserve(tris.size() * 3);
279
307
  for (const auto& t : tris) {
280
- for (int i = 0; i < 3; ++i) ++count[undirected(t[i], t[(i + 1) % 3])];
308
+ for (int i = 0; i < 3; ++i) edges.push_back(edgeKey(undirected(t[i], t[(i + 1) % 3])));
281
309
  }
310
+ std::sort(edges.begin(), edges.end());
311
+ const auto usedOnce = [&edges](uint64_t key) {
312
+ const auto lo = std::lower_bound(edges.begin(), edges.end(), key);
313
+ return lo != edges.end() && *lo == key && (lo + 1 == edges.end() || *(lo + 1) != key);
314
+ };
315
+
316
+ std::multimap<Vert, Vert> next; // directed, along the boundary
282
317
  for (const auto& t : tris) {
283
318
  for (int i = 0; i < 3; ++i) {
284
319
  const Vert a = t[i], b = t[(i + 1) % 3];
285
320
  // A boundary edge is walked backwards to close the hole: the
286
321
  // filling faces must wind against the face that owns the edge.
287
- if (count[undirected(a, b)] == 1) next.emplace(b, a);
322
+ if (usedOnce(edgeKey(undirected(a, b)))) next.emplace(b, a);
288
323
  }
289
324
  }
290
325
  std::vector<std::vector<Vert>> loops;
@@ -320,25 +355,45 @@ M repairMesh(const M& mesh, MeshRepairReport& report) {
320
355
  const std::vector<Vert> remap = weldMap(mesh, welded);
321
356
  report.weldedVertices = welded;
322
357
 
323
- std::vector<std::array<Vert, 3>> tris;
324
- std::set<std::array<Vert, 3>> seen;
325
- for (size_t t = 0; t < triCount(mesh); ++t) {
358
+ // Remap every face, then find the duplicates by sorting rather than by
359
+ // inserting each into a std::set -- one tree node per face otherwise.
360
+ // "First occurrence wins" survives because the sort carries the face
361
+ // index and every later member of a run is what gets marked.
362
+ const size_t nTris = triCount(mesh);
363
+ std::vector<std::array<Vert, 3>> remapped(nTris);
364
+ std::vector<char> isDegenerate(nTris, 0), isDuplicate(nTris, 0);
365
+ std::vector<std::pair<std::array<Vert, 3>, size_t>> byShape;
366
+ byShape.reserve(nTris);
367
+ for (size_t t = 0; t < nTris; ++t) {
326
368
  Vert v[3];
327
369
  triVerts(mesh, t, v);
328
370
  std::array<Vert, 3> f{remap[v[0]], remap[v[1]], remap[v[2]]};
371
+ remapped[t] = f;
329
372
  // 2. Degenerate faces, now that welding has collapsed slivers into
330
373
  // repeated indices.
331
374
  if (f[0] == f[1] || f[1] == f[2] || f[0] == f[2]) {
332
- ++report.droppedDegenerate;
375
+ isDegenerate[t] = 1;
333
376
  continue;
334
377
  }
335
378
  std::array<Vert, 3> sorted = f;
336
379
  std::sort(sorted.begin(), sorted.end());
337
- if (!seen.insert(sorted).second) {
380
+ byShape.emplace_back(sorted, t);
381
+ }
382
+ std::sort(byShape.begin(), byShape.end());
383
+ for (size_t i = 1; i < byShape.size(); ++i) {
384
+ if (byShape[i].first == byShape[i - 1].first) isDuplicate[byShape[i].second] = 1;
385
+ }
386
+
387
+ std::vector<std::array<Vert, 3>> tris;
388
+ tris.reserve(nTris);
389
+ for (size_t t = 0; t < nTris; ++t) {
390
+ if (isDegenerate[t]) {
391
+ ++report.droppedDegenerate;
392
+ } else if (isDuplicate[t]) {
338
393
  ++report.droppedDuplicate;
339
- continue;
394
+ } else {
395
+ tris.push_back(remapped[t]);
340
396
  }
341
- tris.push_back(f);
342
397
  }
343
398
 
344
399
  std::vector<size_t> component;
@@ -349,12 +404,19 @@ M repairMesh(const M& mesh, MeshRepairReport& report) {
349
404
  // Before filling, so the new faces are wound against a settled
350
405
  // surface rather than a mixed one.
351
406
  {
352
- std::map<Edge, std::vector<size_t>> edgeTris;
407
+ // Every edge really is needed here (the flood fill walks all of
408
+ // them), so unlike stripSlivers' index this cannot be narrowed --
409
+ // but it can stop allocating a tree node per edge. Sorted flat, then
410
+ // searched by equal_range. Faces still come back in ascending index
411
+ // order within an edge, which the fill's traversal order depends on.
412
+ std::vector<std::pair<uint64_t, size_t>> edgeTris;
413
+ edgeTris.reserve(tris.size() * 3);
353
414
  for (size_t i = 0; i < tris.size(); ++i) {
354
415
  for (int e = 0; e < 3; ++e) {
355
- edgeTris[undirected(tris[i][e], tris[i][(e + 1) % 3])].push_back(i);
416
+ edgeTris.emplace_back(edgeKey(undirected(tris[i][e], tris[i][(e + 1) % 3])), i);
356
417
  }
357
418
  }
419
+ std::sort(edgeTris.begin(), edgeTris.end());
358
420
  component.assign(tris.size(), SIZE_MAX);
359
421
  std::vector<char> done(tris.size(), 0);
360
422
  for (size_t seed = 0; seed < tris.size(); ++seed) {
@@ -368,7 +430,11 @@ M repairMesh(const M& mesh, MeshRepairReport& report) {
368
430
  stack.pop_back();
369
431
  for (int e = 0; e < 3; ++e) {
370
432
  const Vert a = tris[i][e], b = tris[i][(e + 1) % 3];
371
- for (size_t j : edgeTris[undirected(a, b)]) {
433
+ const uint64_t key = edgeKey(undirected(a, b));
434
+ const auto lo = std::lower_bound(edgeTris.begin(), edgeTris.end(),
435
+ std::make_pair(key, size_t{0}));
436
+ for (auto it = lo; it != edgeTris.end() && it->first == key; ++it) {
437
+ const size_t j = it->second;
372
438
  if (j == i || done[j]) continue;
373
439
  bool sameDir = false;
374
440
  for (int f = 0; f < 3; ++f) {
@@ -427,22 +493,27 @@ M repairMesh(const M& mesh, MeshRepairReport& report) {
427
493
  // component that was turned inside out and then back reports nothing,
428
494
  // which is what the user sees.
429
495
  {
430
- std::set<std::array<Vert, 3>> before;
431
- for (size_t t = 0; t < triCount(mesh); ++t) {
432
- Vert v[3];
433
- triVerts(mesh, t, v);
434
- before.insert({remap[v[0]], remap[v[1]], remap[v[2]]});
435
- }
436
496
  auto rotated = [](std::array<Vert, 3> f) {
437
497
  // Winding is defined up to rotation, so compare canonically.
438
498
  if (f[1] < f[0] && f[1] <= f[2]) return std::array<Vert, 3>{f[1], f[2], f[0]};
439
499
  if (f[2] < f[0] && f[2] <= f[1]) return std::array<Vert, 3>{f[2], f[0], f[1]};
440
500
  return f;
441
501
  };
442
- std::set<std::array<Vert, 3>> canon;
443
- for (const auto& f : before) canon.insert(rotated(f));
502
+ // Was two std::sets over every face -- the incoming windings and
503
+ // their canonical rotations. One sorted, deduplicated vector answers
504
+ // the same membership questions with no node allocations. Built from
505
+ // `remapped`, which is every ORIGINAL face including the degenerate
506
+ // ones the set also held.
507
+ std::vector<std::array<Vert, 3>> canon;
508
+ canon.reserve(remapped.size());
509
+ for (const auto& f : remapped) canon.push_back(rotated(f));
510
+ std::sort(canon.begin(), canon.end());
511
+ canon.erase(std::unique(canon.begin(), canon.end()), canon.end());
512
+ const auto known = [&canon](const std::array<Vert, 3>& f) {
513
+ return std::binary_search(canon.begin(), canon.end(), f);
514
+ };
444
515
  for (const auto& f : tris) {
445
- if (!canon.count(rotated(f)) && canon.count(rotated({f[0], f[2], f[1]}))) {
516
+ if (!known(rotated(f)) && known(rotated({f[0], f[2], f[1]}))) {
446
517
  ++report.reversedFaces;
447
518
  }
448
519
  }
@@ -2159,3 +2159,35 @@ TEST(BytecodeCompiler, CompileTimeArgumentPlanMatchesTheNameMatchingRules) {
2159
2159
  "echo(count(100));"),
2160
2160
  "ECHO: 5050");
2161
2161
  }
2162
+
2163
+ // A declaration may repeat a parameter name. BOSL2's regular_prism() declares
2164
+ // both `length` and `height` twice, and three of its own documentation
2165
+ // examples stopped rendering when the compile-time argument plan bound only
2166
+ // the first of them -- the body read undef from whichever slot the repeated
2167
+ // name finally resolved to, and asserted. The name-matching path this
2168
+ // replaced filled every parameter of that name, because it walked the
2169
+ // PARAMETERS asking which argument matched, not the other way round.
2170
+ TEST(BytecodeCompiler, ARepeatedParameterNameIsBoundEveryTimeItAppears) {
2171
+ const std::string f = "function f(a, height, b, height) = str(a, \"/\", height, \"/\", b);\n";
2172
+ const std::string g = "function g(height, b, height) = str(height, \"/\", b);\n";
2173
+
2174
+ for (const auto& [src, call] : std::vector<std::pair<std::string, std::string>>{
2175
+ {f, "f(a = 1, height = 20, b = 2)"}, // named
2176
+ {f, "f(1, 20, 2)"}, // positional landing on the first copy
2177
+ {g, "g(20, 2)"}, // repeated name in first position
2178
+ {f, "f(a = 1, b = 2, height = 20)"}, // named, out of order
2179
+ }) {
2180
+ std::string compiled, interpreted;
2181
+ {
2182
+ ScopedVm on(true);
2183
+ compiled = runCapturingEcho(src + "echo(" + call + ");");
2184
+ }
2185
+ {
2186
+ ScopedVm off(false);
2187
+ interpreted = runCapturingEcho(src + "echo(" + call + ");");
2188
+ }
2189
+ EXPECT_EQ(compiled, interpreted) << "VM and interpreter disagree for: " << call;
2190
+ EXPECT_NE(compiled.find("20"), std::string::npos)
2191
+ << call << " lost its repeated-name argument: " << compiled;
2192
+ }
2193
+ }
@@ -163,10 +163,16 @@ TEST(ExportAmf, WritesOneObjectAndOneMaterialPerColour) {
163
163
  }
164
164
 
165
165
  TEST(ExportAmf, TwoObjectsSharingAColourShareOneMaterial) {
166
+ // splitComponents, because two objects is the premise: same-coloured
167
+ // disjoint pieces are ONE object by default (see the DisjointPieces
168
+ // tests below). What is under test here is that the two of them share a
169
+ // single material entry rather than each declaring their own.
166
170
  std::vector<ColoredBody> bodies =
167
171
  evalToBodies("color(\"red\") cube(2); translate([10,0,0]) color(\"red\") cube(2);");
168
172
  const std::string path = tempPath("shared.amf").string();
169
- exportModel(path, bodies, ExportOptions{});
173
+ ExportOptions opts;
174
+ opts.splitComponents = true;
175
+ exportModel(path, bodies, opts);
170
176
 
171
177
  const std::string xml = readFile(path);
172
178
  EXPECT_EQ(countOf(xml, "<object id="), 2u);
@@ -174,6 +180,50 @@ TEST(ExportAmf, TwoObjectsSharingAColourShareOneMaterial) {
174
180
  std::remove(path.c_str());
175
181
  }
176
182
 
183
+ // Issue #319: a model in several disconnected pieces was written as one
184
+ // object PER PIECE, so a slicer listed them individually -- hundreds of
185
+ // entries for a multiboard tile, and Prusa Slicer objecting. OpenSCAD writes
186
+ // one object, and so does this by default now; the split is still available
187
+ // for callers that want the pieces apart.
188
+ TEST(ExportSplit, DisjointPiecesAreOneObjectByDefault) {
189
+ std::vector<ColoredBody> bodies =
190
+ evalToBodies("cube(10); translate([20,0,0]) cube(10); translate([40,0,0]) cube(10);");
191
+ const std::string path = tempPath("joined.amf").string();
192
+ exportModel(path, bodies, ExportOptions{});
193
+
194
+ const std::string xml = readFile(path);
195
+ EXPECT_EQ(countOf(xml, "<object id="), 1u);
196
+ EXPECT_EQ(countOf(xml, "<volume"), 1u);
197
+ std::remove(path.c_str());
198
+ }
199
+
200
+ TEST(ExportSplit, DisjointPiecesSplitWhenAsked) {
201
+ std::vector<ColoredBody> bodies =
202
+ evalToBodies("cube(10); translate([20,0,0]) cube(10); translate([40,0,0]) cube(10);");
203
+ const std::string path = tempPath("split.amf").string();
204
+ ExportOptions opts;
205
+ opts.splitComponents = true;
206
+ exportModel(path, bodies, opts);
207
+
208
+ const std::string xml = readFile(path);
209
+ EXPECT_EQ(countOf(xml, "<object id="), 3u);
210
+ std::remove(path.c_str());
211
+ }
212
+
213
+ // Rule 2 is not what became optional: differently-coloured solids must stay
214
+ // separate objects either way, or a multi-material export loses its colours.
215
+ TEST(ExportSplit, ColoursStayApartWithTheSplitOff) {
216
+ std::vector<ColoredBody> bodies =
217
+ evalToBodies("color(\"red\") cube(10); translate([20,0,0]) color(\"blue\") cube(10);");
218
+ const std::string path = tempPath("colours.amf").string();
219
+ exportModel(path, bodies, ExportOptions{});
220
+
221
+ const std::string xml = readFile(path);
222
+ EXPECT_EQ(countOf(xml, "<object id="), 2u);
223
+ EXPECT_EQ(countOf(xml, "<material id="), 2u);
224
+ std::remove(path.c_str());
225
+ }
226
+
177
227
  TEST(ExportAmf, MaterialAndObjectIdsStartAtOne) {
178
228
  // id 0 is reserved by the spec; a reader may drop anything using it.
179
229
  std::vector<ColoredBody> bodies = evalToBodies("cube(2);");
@@ -358,6 +358,33 @@ TEST(SliverStrip, ANeedleIsRemovedWithoutSplittingAnything) {
358
358
  EXPECT_TRUE(d.manifold()) << d.summary();
359
359
  }
360
360
 
361
+ // Three copies of one position is TWO welds, not one, and the survivor is the
362
+ // lowest-numbered of them. Both facts used to fall out of inserting into a
363
+ // std::map and keeping whatever was already there; they now come from run
364
+ // lengths in a sorted list, where an off-by-one or a wrong representative
365
+ // would be silent -- the geometry is identical either way, only the indices
366
+ // differ.
367
+ TEST(MeshCheck, ThreeCoincidentVerticesAreTwoWeldsOntoTheLowestIndex) {
368
+ manifold::MeshGL m = tetra();
369
+ const uint32_t a = 4, b = 5;
370
+ m.vertProperties.insert(m.vertProperties.end(), {0, 0, 0}); // == vertex 0
371
+ m.vertProperties.insert(m.vertProperties.end(), {0, 0, 0}); // == vertex 0 again
372
+ bool usedA = false;
373
+ for (size_t i = 0; i < m.triVerts.size(); ++i) {
374
+ if (m.triVerts[i] != 0 || i < 6) continue;
375
+ m.triVerts[i] = usedA ? b : a; // spread over both copies
376
+ usedA = !usedA;
377
+ }
378
+
379
+ MeshRepairReport r;
380
+ const manifold::MeshGL fixed = repairMesh(m, r);
381
+ EXPECT_EQ(r.weldedVertices, 2u) << r.summary();
382
+ for (uint32_t v : fixed.triVerts) {
383
+ EXPECT_NE(v, a) << "welded away, so no face may still name it";
384
+ EXPECT_NE(v, b) << "welded away, so no face may still name it";
385
+ }
386
+ }
387
+
361
388
  // import(repair=true) goes through repairMesh, so it has to deal with
362
389
  // slivers too -- an imported STL is exactly where they turn up.
363
390
  TEST(MeshRepair, StripsSliversAndRestitchesThem) {
@@ -633,3 +633,29 @@ def test_degenerate_ranges_do_not_overflow_the_element_count(tmp_path):
633
633
  # Silent for NaN; the unbounded pair warn, with the reference's number.
634
634
  assert len(warnings) == 2, warnings
635
635
  assert all("too many elements (4294967295)" in w for w in warnings), warnings
636
+
637
+
638
+ def test_export_model_facade_accepts_split_components(tmp_path):
639
+ """The Python facade's signature must track the nanobind binding's.
640
+
641
+ They are two hand-written parameter lists for one call, and adding
642
+ `split_components` to the binding alone left the facade dropping it --
643
+ a TypeError the C++ suite cannot see, because it never goes through
644
+ Python. Caught in practice, hence this.
645
+ """
646
+ from openscad_cpp_evaluator import Evaluator, export_model
647
+
648
+ src = tmp_path / "pieces.scad"
649
+ src.write_text("cube(10); translate([20,0,0]) cube(10); translate([40,0,0]) cube(10);")
650
+ ev = Evaluator()
651
+ ev.evaluate(str(src), {})
652
+
653
+ def objects_in(path, **kwargs):
654
+ export_model(str(path), ev.geometry, **kwargs)
655
+ import zipfile
656
+ with zipfile.ZipFile(path) as z:
657
+ return z.read("3D/3dmodel.model").decode().count("<object ")
658
+
659
+ # Default matches OpenSCAD: one object however many pieces.
660
+ assert objects_in(tmp_path / "joined.3mf") == 1
661
+ assert objects_in(tmp_path / "split.3mf", split_components=True) == 3