openscad-cpp-evaluator 1.31.0__tar.gz → 1.32.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 (226) hide show
  1. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/.github/workflows/wheels.yml +9 -0
  2. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/CLAUDE.md +12 -2
  3. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/PKG-INFO +1 -1
  4. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/export.hpp +11 -0
  5. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/pyproject.toml +1 -1
  6. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/export.cpp +108 -1
  7. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_export.cpp +125 -0
  8. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_python_bindings.py +87 -26
  9. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/uv.lock +1 -1
  10. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/.github/workflows/ci.yml +0 -0
  11. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/.gitignore +0 -0
  12. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/.gitmodules +0 -0
  13. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/CMakeLists.txt +0 -0
  14. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/LICENSE +0 -0
  15. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/README.md +0 -0
  16. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/bindings/CMakeLists.txt +0 -0
  17. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/bindings/ast_to_py.cpp +0 -0
  18. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/bindings/module.cpp +0 -0
  19. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/cmake/embed_font.cmake +0 -0
  20. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/examples/CMakeLists.txt +0 -0
  21. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/examples/eval_perf_benchmark.cpp +0 -0
  22. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/examples/manifold_cache_reuse.cpp +0 -0
  23. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/examples/minimal_debugger.cpp +0 -0
  24. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  25. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/.gitignore +0 -0
  26. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  27. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  28. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/README.md +0 -0
  29. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  30. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  31. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  32. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  33. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  34. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  35. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  36. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  37. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  38. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  39. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  40. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  41. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  42. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  43. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  44. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  45. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/.DS_Store +0 -0
  46. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  47. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  48. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  49. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  50. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  51. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  52. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  53. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  54. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  55. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  56. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  57. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  58. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  59. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  60. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  61. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  62. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  63. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  64. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  65. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  66. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  67. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  68. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  69. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  70. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  71. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  72. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  73. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  74. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_concurrent_parse.cpp +0 -0
  75. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  76. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  77. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  78. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  79. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  80. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  81. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  82. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  83. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  84. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  85. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  86. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  87. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  88. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  89. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  90. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  91. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  92. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  93. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  94. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  95. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  96. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  97. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  98. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  99. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  100. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  101. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  102. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  103. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  104. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  105. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  106. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  107. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  108. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  109. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  110. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  111. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  112. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  113. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  114. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  115. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  116. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  117. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  118. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  119. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  120. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  121. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  122. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  123. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  124. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  125. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  126. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  127. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  128. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  129. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  130. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  131. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/resources/fonts/LICENSE +0 -0
  132. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  133. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  134. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  135. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  136. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/CMakeLists.txt +0 -0
  137. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/booleans.cpp +0 -0
  138. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/builtins.hpp +0 -0
  139. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/call_args.cpp +0 -0
  140. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/color.cpp +0 -0
  141. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/control.cpp +0 -0
  142. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/dxf_dim.cpp +0 -0
  143. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/extrude.cpp +0 -0
  144. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/function_builtins.cpp +0 -0
  145. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/import.cpp +0 -0
  146. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/modifiers.cpp +0 -0
  147. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/primitives_2d.cpp +0 -0
  148. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/primitives_3d.cpp +0 -0
  149. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/registry.cpp +0 -0
  150. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/roof.cpp +0 -0
  151. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/segments.cpp +0 -0
  152. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/surface.cpp +0 -0
  153. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/text.cpp +0 -0
  154. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/topology.cpp +0 -0
  155. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/builtins/transforms.cpp +0 -0
  156. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/bytecode_compiler.cpp +0 -0
  157. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/bytecode_vm.cpp +0 -0
  158. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/colored_body.cpp +0 -0
  159. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/coverage.cpp +0 -0
  160. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/csg_generate.cpp +0 -0
  161. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/csg_resolve.cpp +0 -0
  162. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/css_colors.cpp +0 -0
  163. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/debug/debug_repl.cpp +0 -0
  164. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/debug_profile.cpp +0 -0
  165. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/eval_context.cpp +0 -0
  166. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/eval_error.cpp +0 -0
  167. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/eval_use.cpp +0 -0
  168. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/expr_eval.cpp +0 -0
  169. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/format_closure.cpp +0 -0
  170. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/import/dxf_import.cpp +0 -0
  171. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/import/mesh_import.cpp +0 -0
  172. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/import/surface_load.cpp +0 -0
  173. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/import/svg_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/manifold_cache.cpp +0 -0
  175. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/measure_geometry.cpp +0 -0
  176. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/mesh_check.cpp +0 -0
  177. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/stmt_eval.cpp +0 -0
  178. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/text/font_match.cpp +0 -0
  179. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/text/freetype_font_provider.cpp +0 -0
  180. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/text/text_metrics.cpp +0 -0
  181. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/user_calls.cpp +0 -0
  182. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/utf8.cpp +0 -0
  183. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/value.cpp +0 -0
  184. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/xkcd_colors.cpp +0 -0
  185. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/xkcd_colors.hpp +0 -0
  186. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/src/zip_stored.cpp +0 -0
  187. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/CMakeLists.txt +0 -0
  188. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_booleans.cpp +0 -0
  189. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_bytecode_compiler.cpp +0 -0
  190. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_children_separate.cpp +0 -0
  191. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_cli.cpp +0 -0
  192. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_control_flow.cpp +0 -0
  193. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_coverage.cpp +0 -0
  194. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_csg_tree.cpp +0 -0
  195. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_debug_hooks.cpp +0 -0
  196. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_discretizer.cpp +0 -0
  197. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_dxf_svg_import.cpp +0 -0
  198. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_eval_context.cpp +0 -0
  199. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_expr_eval.cpp +0 -0
  200. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_extrude_roof.cpp +0 -0
  201. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_feature_detection.cpp +0 -0
  202. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_function_builtins.cpp +0 -0
  203. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_helpers.hpp +0 -0
  204. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_import_export.cpp +0 -0
  205. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_include_cache.cpp +0 -0
  206. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_manifold_cache.cpp +0 -0
  207. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_mesh_check.cpp +0 -0
  208. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_multi_color_merge.cpp +0 -0
  209. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_primitives.cpp +0 -0
  210. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_profiling.cpp +0 -0
  211. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_render_expr.cpp +0 -0
  212. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_scope_trail.cpp +0 -0
  213. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_scoping.cpp +0 -0
  214. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_surface.cpp +0 -0
  215. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_tail_calls.cpp +0 -0
  216. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_text.cpp +0 -0
  217. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_transforms.cpp +0 -0
  218. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_unexpected_args.cpp +0 -0
  219. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_use.cpp +0 -0
  220. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_value.cpp +0 -0
  221. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_viewport_params.cpp +0 -0
  222. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tests/test_zip_stored.cpp +0 -0
  223. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tools/cli/CMakeLists.txt +0 -0
  224. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tools/cli/cli_lib.cpp +0 -0
  225. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tools/cli/cli_lib.hpp +0 -0
  226. {openscad_cpp_evaluator-1.31.0 → openscad_cpp_evaluator-1.32.0}/tools/cli/main.cpp +0 -0
@@ -119,6 +119,9 @@ jobs:
119
119
  # C:\msys64 (not on PATH); its bison/flex satisfy the parser
120
120
  # submodule's grammar version requirement.
121
121
  CIBW_BEFORE_ALL_WINDOWS: 'C:\msys64\usr\bin\bash.exe -lc "pacman -Sy --noconfirm bison flex"'
122
+ # The binding tests run under pytest (the file's own entry point
123
+ # hands over to it): most of them take tmp_path or use pytest.raises.
124
+ CIBW_TEST_REQUIRES: "pytest"
122
125
  CIBW_TEST_COMMAND: "python {package}/tests/test_python_bindings.py"
123
126
 
124
127
  - uses: actions/upload-artifact@v7
@@ -128,6 +131,9 @@ jobs:
128
131
 
129
132
  publish-testpypi:
130
133
  needs: [sdist, build_wheels]
134
+ # Tags only: a manual dispatch builds and tests every wheel, and must
135
+ # never publish whatever branch it was run on.
136
+ if: startsWith(github.ref, 'refs/tags/v')
131
137
  runs-on: ubuntu-latest
132
138
  environment: testpypi
133
139
  steps:
@@ -164,6 +170,9 @@ jobs:
164
170
 
165
171
  publish-pypi:
166
172
  needs: [publish-testpypi]
173
+ # Tags only: a manual dispatch builds and tests every wheel, and must
174
+ # never publish whatever branch it was run on.
175
+ if: startsWith(github.ref, 'refs/tags/v')
167
176
  runs-on: ubuntu-latest
168
177
  # The "pypi" environment carries no protection rules, so a tag runs
169
178
  # straight through TestPyPI to PyPI with nothing to stop it -- and a
@@ -1091,8 +1091,8 @@ grep for `ponytail:`.
1091
1091
  — skip it and the mesh comes out non-manifold at the seam). Self-contained 2D geometry math here
1092
1092
  (`dot2`/`len2`/`norm2`, the parabola-canonical-frame rotation in `discretizeArc`) exists nowhere
1093
1093
  else in this codebase.
1094
- - `include/openscad_cpp_evaluator/export.hpp`, `src/export.cpp` — the writers for all ten
1095
- formats `exportExtensions()` lists: 3MF, AMF, PDF, STL, OBJ, OFF, PLY, SVG, VRML, X3D (3MF via
1094
+ - `include/openscad_cpp_evaluator/export.hpp`, `src/export.cpp` — the writers for all eleven
1095
+ formats `exportExtensions()` lists: 3MF, AMF, DXF, PDF, STL, OBJ, OFF, PLY, SVG, VRML, X3D (3MF via
1096
1096
  `zip_stored.hpp`'s `writeDeflateZip`). **`exportExtensions()` is the one source of truth for
1097
1097
  which formats exist** — the Python facade and BelfrySCAD's dialog/CLI all ask it rather than
1098
1098
  restating the list, because three hand-written copies had already drifted far enough that `.off`
@@ -1120,6 +1120,16 @@ grep for `ponytail:`.
1120
1120
  binary's, vertex order and 6-vertex line wrap included. A 3D body in the list throws
1121
1121
  ("Current top level object is not a 2D object"), as OpenSCAD refuses too: there is no projection
1122
1122
  to fall back on. `ExportOptions::svg` carries OpenSCAD's five `-O export-svg/...` settings.
1123
+ **`writeDxf` is the third 2D writer**, also on `collect2d()`: OpenSCAD's own `export_dxf.cc`
1124
+ layout verbatim (AC1006 header with the extents, one CONTINUOUS linetype, layer 0, every contour
1125
+ a closed `LWPOLYLINE` on layer 0 with holes as ordinary contours; 1- and 2-vertex contours as
1126
+ `POINT`/`LINE`), pinned byte-for-byte by `ExportDxf.MatchesRealOpenscadByteForByte`. Two
1127
+ deliberate differences: coordinates are the **shortest text that round-trips the double**
1128
+ (`dxfNum`), not OpenSCAD's 6 significant digits -- a DXF feeds a cutter, and its circles do not
1129
+ survive a re-import otherwise; and `$EXTMAX` is right for a model below zero (OpenSCAD seeds its
1130
+ maxima with `numeric_limits<double>::min()`, the smallest POSITIVE double). Every number goes
1131
+ through the classic locale, the file stream included: Qt sets the process locale, and a grouping
1132
+ locale writes a 1500-vertex count as `1.500`.
1123
1133
  **`writePdf` is the other 2D writer**, and shares `collect2d()` with `writeSvg` so the all-2D
1124
1134
  rule and its refusal have one implementation. Everything else differs from SVG: the drawing is
1125
1135
  **centred on a fixed paper size** rather than the page being cut to fit, and **PDF user space is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.31.0
3
+ Version: 1.32.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -158,6 +158,17 @@ struct ExportSvgOptions {
158
158
  void writeSvg(const std::string& path, const std::vector<ColoredBody>& bodies,
159
159
  const ExportSvgOptions& opts = {});
160
160
 
161
+ // DXF, the third 2D writer: OpenSCAD's own export_dxf.cc layout (an AC1006
162
+ // header with the model's extents, one CONTINUOUS linetype, layer 0) and
163
+ // every contour of every body a closed LWPOLYLINE on layer 0 -- holes are
164
+ // contours like any other, as in OpenSCAD. A 1-vertex contour is a POINT
165
+ // and a 2-vertex one a LINE, again as OpenSCAD writes them. Shares
166
+ // collect2d() with writeSvg/writePdf, so a 3D model is refused the same
167
+ // way. Coordinates are written at full round-trip precision, NOT OpenSCAD's
168
+ // 6 significant digits: a DXF goes to a cutter, where 1234.567 written as
169
+ // 1234.57 is a real error.
170
+ void writeDxf(const std::string& path, const std::vector<ColoredBody>& bodies);
171
+
161
172
  // Options for the PDF writer, named and defaulted after OpenSCAD's own
162
173
  // `-O export-pdf/...` set.
163
174
  struct ExportPdfOptions {
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.31.0"
7
+ version = "1.32.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -13,6 +13,8 @@
13
13
  #include <cstdio>
14
14
  #include <ctime>
15
15
  #include <fstream>
16
+ #include <locale>
17
+ #include <sstream>
16
18
  #include <optional>
17
19
  #include <stdexcept>
18
20
 
@@ -1155,6 +1157,105 @@ void writeSvg(const std::string& path, const std::vector<ColoredBody>& bodies, c
1155
1157
 
1156
1158
  namespace {
1157
1159
 
1160
+ // The shortest decimal that reads back as exactly `v`, in the classic
1161
+ // locale (Qt sets the process locale from the environment, so printf could
1162
+ // write "1,5" on a German system and no DXF reader would take it). -0 is
1163
+ // written as 0, and a non-finite value -- which no contour should hold --
1164
+ // as 0 rather than as text a reader would choke on.
1165
+ std::string dxfNum(double v) {
1166
+ if (!std::isfinite(v) || v == 0.0) return "0";
1167
+ for (int digits = 6; digits <= 17; ++digits) {
1168
+ std::ostringstream o;
1169
+ o.imbue(std::locale::classic());
1170
+ o.precision(digits);
1171
+ o << v;
1172
+ std::istringstream in(o.str());
1173
+ in.imbue(std::locale::classic());
1174
+ double back = 0.0;
1175
+ in >> back;
1176
+ if (back == v) return o.str();
1177
+ }
1178
+ std::ostringstream o; // unreachable: 17 digits always round-trips a double
1179
+ o.imbue(std::locale::classic());
1180
+ o.precision(17);
1181
+ o << v;
1182
+ return o.str();
1183
+ }
1184
+
1185
+ // The header, tables and (empty) blocks sections, verbatim from OpenSCAD's
1186
+ // export_dxf.cc -- its comments record which readers each piece was needed
1187
+ // for (Inkscape, LibreCAD, Illustrator, sharecad.org, generic cutters).
1188
+ void writeDxfHeader(std::ostream& out, double xMin, double yMin, double xMax, double yMax) {
1189
+ out << "999\nDXF from OpenSCAD\n"
1190
+ << " 0\nSECTION\n 2\nHEADER\n"
1191
+ << " 9\n$ACADVER\n 1\nAC1006\n"
1192
+ << " 9\n$INSBASE\n 10\n0.0\n 20\n0.0\n 30\n0.0\n";
1193
+ for (const char* name : {"$EXTMIN", "$EXTMAX", "$LINMIN", "$LINMAX"}) {
1194
+ const bool isMin = std::string(name).find("MIN") != std::string::npos;
1195
+ out << " 9\n" << name << "\n 10\n" << dxfNum(isMin ? xMin : xMax) << "\n 20\n"
1196
+ << dxfNum(isMin ? yMin : yMax) << "\n";
1197
+ }
1198
+ out << " 0\nENDSEC\n"
1199
+ << " 0\nSECTION\n 2\nTABLES\n"
1200
+ << " 0\nTABLE\n 2\nLTYPE\n 70\n1\n"
1201
+ << " 0\nLTYPE\n 2\nCONTINUOUS\n 70\n64\n 3\nSolid line\n 72\n65\n 73\n0\n 40\n0.000000\n"
1202
+ << " 0\nENDTAB\n"
1203
+ << " 0\nTABLE\n 2\nLAYER\n 70\n6\n"
1204
+ << " 0\nLAYER\n 2\n0\n 70\n64\n 62\n7\n 6\nCONTINUOUS\n"
1205
+ << " 0\nENDTAB\n"
1206
+ << " 0\nTABLE\n 2\nSTYLE\n 70\n0\n 0\nENDTAB\n"
1207
+ << " 0\nENDSEC\n"
1208
+ << " 0\nSECTION\n 2\nBLOCKS\n 0\nENDSEC\n";
1209
+ }
1210
+
1211
+ } // namespace
1212
+
1213
+ void writeDxf(const std::string& path, const std::vector<ColoredBody>& bodies) {
1214
+ const Flat2d flat = collect2d(bodies);
1215
+
1216
+ std::ofstream out(path);
1217
+ if (!out) throw std::runtime_error("Could not open '" + path + "' for writing");
1218
+ // The vertex counts go through the stream itself, which otherwise takes
1219
+ // the global C++ locale -- one with digit grouping writes 1500 as "1.500".
1220
+ out.imbue(std::locale::classic());
1221
+
1222
+ // Extents from collect2d's own bounding box. OpenSCAD starts its maxima
1223
+ // at numeric_limits<double>::min() -- the smallest POSITIVE double, not
1224
+ // the most negative -- so a model lying entirely at negative x or y gets
1225
+ // a wrong $EXTMAX there. That is a bug, not a format rule.
1226
+ writeDxfHeader(out, flat.minx, flat.miny, flat.maxx, flat.maxy);
1227
+
1228
+ out << " 0\nSECTION\n 2\nENTITIES\n";
1229
+ for (const manifold::Polygons& polys : flat.perBody) {
1230
+ for (const manifold::SimplePolygon& contour : polys) {
1231
+ const auto xy = [&](const char* xCode, const char* yCode, const manifold::vec2& p) {
1232
+ out << xCode << "\n" << dxfNum(p.x) << "\n" << yCode << "\n" << dxfNum(p.y) << "\n";
1233
+ };
1234
+ switch (contour.size()) {
1235
+ case 0:
1236
+ break;
1237
+ case 1:
1238
+ out << " 0\nPOINT\n100\nAcDbEntity\n 8\n0\n100\nAcDbPoint\n";
1239
+ xy(" 10", " 20", contour[0]);
1240
+ break;
1241
+ case 2:
1242
+ // [X1 Y1 X2 Y2]: the order ezdxf and dxfgrabber can read.
1243
+ out << " 0\nLINE\n100\nAcDbEntity\n 8\n0\n100\nAcDbLine\n";
1244
+ xy(" 10", " 20", contour[0]);
1245
+ xy(" 11", " 21", contour[1]);
1246
+ break;
1247
+ default:
1248
+ out << " 0\nLWPOLYLINE\n100\nAcDbEntity\n 8\n0\n100\nAcDbPolyline\n"
1249
+ << " 90\n" << contour.size() << "\n 70\n1\n"; // 70 = 1: closed
1250
+ for (const manifold::vec2& p : contour) xy(" 10", " 20", p);
1251
+ }
1252
+ }
1253
+ }
1254
+ out << " 0\nENDSEC\n 0\nEOF\n";
1255
+ }
1256
+
1257
+ namespace {
1258
+
1158
1259
  // PDF's own unit is 1/72 inch; models are millimetres.
1159
1260
  constexpr double kPtPerMm = 72.0 / 25.4;
1160
1261
  // Page margin the ruler is drawn on, in points. OpenSCAD's own MARGIN.
@@ -1497,7 +1598,7 @@ std::vector<std::string> writePdf(const std::string& path, const std::vector<Col
1497
1598
  }
1498
1599
 
1499
1600
  const std::vector<std::string>& exportExtensions() {
1500
- static const std::vector<std::string> exts = {".3mf", ".amf", ".pdf", ".stl", ".obj",
1601
+ static const std::vector<std::string> exts = {".3mf", ".amf", ".dxf", ".pdf", ".stl", ".obj",
1501
1602
  ".off", ".ply", ".svg", ".wrl", ".x3d"};
1502
1603
  return exts;
1503
1604
  }
@@ -1524,6 +1625,12 @@ std::vector<std::string> exportModel(const std::string& path, const std::vector<
1524
1625
  return writePdf(path, bodies, opts.pdf);
1525
1626
  }
1526
1627
 
1628
+ if (ext == ".dxf") {
1629
+ // 2D, outside the mesh pipeline like SVG and PDF.
1630
+ writeDxf(path, bodies);
1631
+ return warnings;
1632
+ }
1633
+
1527
1634
  if (ext == ".svg") {
1528
1635
  // 2D, and nothing the mesh pipeline below does applies: no merge,
1529
1636
  // no sliver strip, no manifoldness check. Contours are what they
@@ -10,6 +10,7 @@
10
10
  #include <cstdio>
11
11
  #include <filesystem>
12
12
  #include <fstream>
13
+ #include <locale>
13
14
  #include <gtest/gtest.h>
14
15
  #include <sstream>
15
16
  #include <stb_image.h>
@@ -444,6 +445,130 @@ M 18.0866,-7.3806 L 16.4645,-8.46447 L 15.3806,-10.0866 L 15,-12 L 15.3806,-13.9
444
445
  expected);
445
446
  }
446
447
 
448
+ // -- DXF (2D) --------------------------------------------------------------
449
+
450
+ namespace {
451
+
452
+ std::string dxfOf(const std::string& code, const std::string& name) {
453
+ const std::string path = tempPath(name).string();
454
+ writeDxf(path, evalToBodies(code));
455
+ const std::string text = readText(path);
456
+ std::remove(path.c_str());
457
+ return text;
458
+ }
459
+
460
+ // The value on the line after the first `code` line following `header`
461
+ // (DXF is group-code / value pairs, one per line).
462
+ std::string dxfValueAfter(const std::string& text, const std::string& header, const std::string& code) {
463
+ size_t at = text.find("\n" + header + "\n");
464
+ if (at == std::string::npos) return "";
465
+ at = text.find("\n" + code + "\n", at + 1);
466
+ if (at == std::string::npos) return "";
467
+ const size_t start = at + code.size() + 2;
468
+ return text.substr(start, text.find('\n', start) - start);
469
+ }
470
+
471
+ } // namespace
472
+
473
+ // Byte-for-byte against real OpenSCAD 2026.02.01's `-o out.dxf` for the same
474
+ // script: header, tables, entity layout, contour order and vertex order. The
475
+ // coordinates are chosen to need no more than 6 significant digits, the only
476
+ // place ours deliberately differs (see FullPrecisionNotSixDigits).
477
+ TEST(ExportDxf, MatchesRealOpenscadByteForByte) {
478
+ const std::string expected =
479
+ "999\nDXF from OpenSCAD\n 0\nSECTION\n 2\nHEADER\n 9\n$ACADVER\n 1\nAC1006\n"
480
+ " 9\n$INSBASE\n 10\n0.0\n 20\n0.0\n 30\n0.0\n"
481
+ " 9\n$EXTMIN\n 10\n0\n 20\n0\n 9\n$EXTMAX\n 10\n40\n 20\n25\n"
482
+ " 9\n$LINMIN\n 10\n0\n 20\n0\n 9\n$LINMAX\n 10\n40\n 20\n25\n"
483
+ " 0\nENDSEC\n 0\nSECTION\n 2\nTABLES\n 0\nTABLE\n 2\nLTYPE\n 70\n1\n"
484
+ " 0\nLTYPE\n 2\nCONTINUOUS\n 70\n64\n 3\nSolid line\n 72\n65\n 73\n0\n 40\n0.000000\n"
485
+ " 0\nENDTAB\n 0\nTABLE\n 2\nLAYER\n 70\n6\n 0\nLAYER\n 2\n0\n 70\n64\n 62\n7\n 6\nCONTINUOUS\n"
486
+ " 0\nENDTAB\n 0\nTABLE\n 2\nSTYLE\n 70\n0\n 0\nENDTAB\n 0\nENDSEC\n"
487
+ " 0\nSECTION\n 2\nBLOCKS\n 0\nENDSEC\n 0\nSECTION\n 2\nENTITIES\n"
488
+ " 0\nLWPOLYLINE\n100\nAcDbEntity\n 8\n0\n100\nAcDbPolyline\n 90\n4\n 70\n1\n"
489
+ " 10\n40\n 20\n25\n 10\n0\n 20\n25\n 10\n0\n 20\n0\n 10\n40\n 20\n0\n"
490
+ " 0\nLWPOLYLINE\n100\nAcDbEntity\n 8\n0\n100\nAcDbPolyline\n 90\n4\n 70\n1\n"
491
+ " 10\n10\n 20\n5\n 10\n10\n 20\n17.5\n 10\n30\n 20\n17.5\n 10\n30\n 20\n5\n"
492
+ " 0\nENDSEC\n 0\nEOF\n";
493
+ EXPECT_EQ(dxfOf("difference() { square([40,25]); translate([10,5]) square([20,12.5]); }", "parity.dxf"),
494
+ expected);
495
+ }
496
+
497
+ // OpenSCAD streams doubles at the default 6 significant digits, so 1234.5678
498
+ // reaches the file as 1234.57 -- a 2 micron error on a cut part, from the
499
+ // file format alone. Ours writes the shortest text that reads back exactly.
500
+ TEST(ExportDxf, FullPrecisionNotSixDigits) {
501
+ const std::string text = dxfOf("square([1234.5678, 0.1]);", "precise.dxf");
502
+ EXPECT_EQ(dxfValueAfter(text, "$EXTMAX", " 10"), "1234.5678");
503
+ EXPECT_EQ(dxfValueAfter(text, "$EXTMAX", " 20"), "0.1");
504
+ EXPECT_EQ(countOf(text, "\n1234.5678\n"), 4u); // $EXTMAX, $LINMAX and two vertices
505
+ }
506
+
507
+ // OpenSCAD starts its maxima at numeric_limits<double>::min(), the smallest
508
+ // POSITIVE double, so a model entirely below zero reports $EXTMAX of ~0.
509
+ TEST(ExportDxf, ExtentsOfAModelBelowZero) {
510
+ const std::string text = dxfOf("translate([-50,-30]) square(10);", "negative.dxf");
511
+ EXPECT_EQ(dxfValueAfter(text, "$EXTMIN", " 10"), "-50");
512
+ EXPECT_EQ(dxfValueAfter(text, "$EXTMIN", " 20"), "-30");
513
+ EXPECT_EQ(dxfValueAfter(text, "$EXTMAX", " 10"), "-40");
514
+ EXPECT_EQ(dxfValueAfter(text, "$EXTMAX", " 20"), "-20");
515
+ }
516
+
517
+ TEST(ExportDxf, EveryContourIsAClosedPolylineHolesIncluded) {
518
+ const std::string text =
519
+ dxfOf("difference() { circle(10, $fn=12); circle(5, $fn=12); } translate([30,0]) square(4);", "multi.dxf");
520
+ EXPECT_EQ(countOf(text, "LWPOLYLINE"), 3u); // outer, hole, and the separate square
521
+ EXPECT_EQ(countOf(text, " 90\n12\n 70\n1\n"), 2u); // both circles: 12 vertices, closed
522
+ EXPECT_EQ(countOf(text, " 90\n4\n 70\n1\n"), 1u);
523
+ }
524
+
525
+ namespace {
526
+ // A locale that writes 1500.5 as "1.500,5" -- what a German global locale
527
+ // would do to any number that reaches a stream without the classic locale.
528
+ struct GermanishPunct : std::numpunct<char> {
529
+ char do_decimal_point() const override { return ','; }
530
+ char do_thousands_sep() const override { return '.'; }
531
+ std::string do_grouping() const override { return "\3"; }
532
+ };
533
+ } // namespace
534
+
535
+ TEST(ExportDxf, NumbersIgnoreTheGlobalLocale) {
536
+ const std::locale previous = std::locale::global(std::locale(std::locale::classic(), new GermanishPunct));
537
+ std::string text;
538
+ try {
539
+ text = dxfOf("translate([0.5,0]) circle(1000, $fn=1500);", "locale.dxf");
540
+ } catch (...) {
541
+ std::locale::global(previous);
542
+ throw;
543
+ }
544
+ std::locale::global(previous);
545
+ EXPECT_NE(text.find(" 90\n1500\n"), std::string::npos);
546
+ EXPECT_EQ(dxfValueAfter(text, "$EXTMAX", " 10"), "1000.5");
547
+ EXPECT_EQ(text.find(','), std::string::npos);
548
+ }
549
+
550
+ TEST(ExportDxf, RefusesA3dModel) {
551
+ std::vector<ColoredBody> bodies = evalToBodies("cube(10);");
552
+ EXPECT_THROW(writeDxf(tempPath("solid.dxf").string(), bodies), std::runtime_error);
553
+ }
554
+
555
+ TEST(ExportDxf, NoGeometryThrows) {
556
+ std::vector<ColoredBody> empty;
557
+ EXPECT_THROW(writeDxf(tempPath("empty.dxf").string(), empty), std::runtime_error);
558
+ }
559
+
560
+ TEST(ExportDxf, ReachableThroughExportModelAndListedAsAnExtension) {
561
+ const std::vector<std::string>& exts = exportExtensions();
562
+ EXPECT_NE(std::find(exts.begin(), exts.end(), ".dxf"), exts.end());
563
+ const std::string path = tempPath("via_export_model.dxf").string();
564
+ const std::vector<std::string> warnings = exportModel(path, evalToBodies("square(5);"), ExportOptions{});
565
+ EXPECT_TRUE(warnings.empty());
566
+ const std::string text = readText(path);
567
+ EXPECT_EQ(countOf(text, "LWPOLYLINE"), 1u);
568
+ EXPECT_NE(text.find(" 0\nEOF\n"), std::string::npos);
569
+ std::remove(path.c_str());
570
+ }
571
+
447
572
  // -- PDF (2D) --------------------------------------------------------------
448
573
 
449
574
  namespace {
@@ -6,12 +6,12 @@ test_debug_hooks.cpp, test_viewport_params.cpp); this file only exercises the
6
6
  nanobind plumbing that exposes them to Python, which has no coverage of its
7
7
  own otherwise.
8
8
 
9
- No test framework: plain functions + assert, run via `python
10
- tests/test_python_bindings.py`. This project has no existing Python test
11
- infrastructure (pure C++/gtest via CMake/ctest) -- adding pytest just for
12
- this would be a new dependency for a handful of checks; wired into
13
- wheels.yml's CIBW_TEST_COMMAND instead, which already builds+installs the
14
- package on every release platform.
9
+ Run with `python tests/test_python_bindings.py` (what wheels.yml's
10
+ CIBW_TEST_COMMAND does on every release platform) or pytest directly; the
11
+ entry point hands the file to pytest. It started as plain functions with a
12
+ hand-rolled runner, but the file outgrew that: most tests now take tmp_path
13
+ or use pytest.raises, and the runner, sitting part-way down, never reached
14
+ them in a wheel build at all.
15
15
  """
16
16
  import os
17
17
  import sys
@@ -20,6 +20,25 @@ from pathlib import Path
20
20
 
21
21
  from openscad_cpp_evaluator import Evaluator, ManifoldCache, format_csg_tree
22
22
 
23
+ import pytest
24
+
25
+
26
+ @pytest.fixture(autouse=True)
27
+ def _scripts_are_utf8_with_lf(monkeypatch):
28
+ """Every .scad these tests write is UTF-8 with \\n line endings, as any
29
+ editor saves one. Path.write_text's own defaults are the platform's:
30
+ cp1252 and CRLF on Windows, which made "aé—z" undecodable and shifted
31
+ every source span after a line break by one character per line against
32
+ the test's own string -- failures of the test, not of the evaluator. An
33
+ explicit encoding or newline still wins."""
34
+ orig = Path.write_text
35
+
36
+ def write_text(self, data, encoding=None, errors=None, newline=None):
37
+ return orig(self, data, encoding=encoding or "utf-8", errors=errors,
38
+ newline="\n" if newline is None else newline)
39
+
40
+ monkeypatch.setattr(Path, "write_text", write_text)
41
+
23
42
 
24
43
  def _write(src: str) -> str:
25
44
  f = tempfile.NamedTemporaryFile(suffix=".scad", mode="w", encoding="utf-8", delete=False)
@@ -389,25 +408,6 @@ def test_format_source_raises_parse_error_on_bad_input():
389
408
  raise AssertionError("expected ParseError")
390
409
 
391
410
 
392
- def main():
393
- tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
394
- failures = []
395
- for t in tests:
396
- try:
397
- t()
398
- print(f"PASS {t.__name__}")
399
- except Exception as e:
400
- failures.append((t.__name__, e))
401
- print(f"FAIL {t.__name__}: {e}")
402
- print(f"\n{len(tests) - len(failures)}/{len(tests)} passed")
403
- if failures:
404
- sys.exit(1)
405
-
406
-
407
- if __name__ == "__main__":
408
- main()
409
-
410
-
411
411
  def test_evaluate_generate_false_skips_geometry(tmp_path):
412
412
  """generate=False runs the script but builds no Manifold geometry.
413
413
 
@@ -702,6 +702,51 @@ def test_export_model_svg_refuses_a_3d_model(tmp_path):
702
702
  export_model(str(tmp_path / "solid.svg"), ev.geometry)
703
703
 
704
704
 
705
+ def test_export_model_writes_dxf_that_imports_back_unchanged(tmp_path):
706
+ """.dxf through the facade, then back in through import(): exporting what
707
+ was imported must give the same file, which only holds if every
708
+ coordinate survived at full precision (OpenSCAD's own writer keeps 6
709
+ significant digits, so its circles do not round-trip)."""
710
+ from openscad_cpp_evaluator import Evaluator, export_extensions, export_model
711
+
712
+ assert ".dxf" in export_extensions()
713
+
714
+ def export(script, name):
715
+ src = tmp_path / (name + ".scad")
716
+ src.write_text(script)
717
+ ev = Evaluator()
718
+ ev.evaluate(str(src), {})
719
+ out = tmp_path / (name + ".dxf")
720
+ assert export_model(str(out), ev.geometry) == []
721
+ return out
722
+
723
+ first = export("difference() { square([40,25]); translate([20,12]) circle(5, $fn=16); }", "first")
724
+ text = first.read_text()
725
+ assert text.startswith("999\nDXF from OpenSCAD\n") and text.endswith(" 0\nEOF\n")
726
+ assert text.count("LWPOLYLINE") == 2
727
+ # The circle's vertices carry more than 6 significant digits -- the ones
728
+ # OpenSCAD's writer would cut them to. (Their exact values are whatever
729
+ # Manifold's 2D kernel rounded them to, so no literal is pinned here.)
730
+ lines = text.split("\n")
731
+ coords = [lines[i + 1] for i, code in enumerate(lines[:-1]) if code in (" 10", " 20")]
732
+ assert max(len(c.lstrip("-").replace(".", "").lstrip("0")) for c in coords) > 6
733
+
734
+ second = export(f'import("{first.as_posix()}");', "second")
735
+ assert second.read_text() == text
736
+
737
+
738
+ def test_export_model_dxf_refuses_a_3d_model(tmp_path):
739
+ import pytest
740
+ from openscad_cpp_evaluator import Evaluator, EvalError, export_model
741
+
742
+ src = tmp_path / "solid.scad"
743
+ src.write_text("cube(10);")
744
+ ev = Evaluator()
745
+ ev.evaluate(str(src), {})
746
+ with pytest.raises(EvalError, match="not a 2D object"):
747
+ export_model(str(tmp_path / "solid.dxf"), ev.geometry)
748
+
749
+
705
750
  def test_export_model_writes_pdf_and_takes_its_options(tmp_path):
706
751
  """.pdf through the facade, plus the option dict. Same
707
752
  two-parameter-list trap as split_components and the svg_* set."""
@@ -1011,7 +1056,8 @@ def _svg_counts(tmp_path, args):
1011
1056
  svg = tmp_path / "f.svg"
1012
1057
  svg.write_text(SVG_FILTER)
1013
1058
  scad = tmp_path / "f.scad"
1014
- scad.write_text(f'echo(n = len(import("{svg}"{args})));\n')
1059
+ # as_posix: a Windows path's backslashes would be escapes in a .scad string.
1060
+ scad.write_text(f'echo(n = len(import("{svg.as_posix()}"{args})));\n')
1015
1061
  msgs = []
1016
1062
  Evaluator(echo_fn=msgs.append).evaluate(str(scad), generate=False)
1017
1063
  echoes = [m for m in msgs if m.startswith("ECHO:")]
@@ -1177,3 +1223,18 @@ def test_a_cancel_signal_stops_a_silent_script_from_another_thread(tmp_path):
1177
1223
  except EvalError as e:
1178
1224
  assert "Render cancelled" in str(e), e
1179
1225
  assert time.perf_counter() - t0 < 5
1226
+
1227
+
1228
+ def main():
1229
+ """`python tests/test_python_bindings.py` -- how cibuildwheel runs this
1230
+ file -- runs the whole file under pytest. It used to loop over the test_
1231
+ functions itself, from a main() placed part-way down: every test defined
1232
+ below that point (37 of them, every export test included) never ran in a
1233
+ wheel build, and the ones that take tmp_path could not have."""
1234
+ import pytest
1235
+
1236
+ sys.exit(pytest.main([__file__, "-q"]))
1237
+
1238
+
1239
+ if __name__ == "__main__":
1240
+ main()
@@ -55,7 +55,7 @@ wheels = [
55
55
 
56
56
  [[package]]
57
57
  name = "openscad-cpp-evaluator"
58
- version = "1.31.0"
58
+ version = "1.32.0"
59
59
  source = { editable = "." }
60
60
  dependencies = [
61
61
  { name = "numpy" },