openscad-cpp-evaluator 1.9.0__tar.gz → 1.10.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 (215) hide show
  1. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/CLAUDE.md +17 -6
  2. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/bindings/module.cpp +14 -3
  4. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/colored_body.hpp +4 -0
  5. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/export.hpp +32 -0
  6. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/pyproject.toml +1 -1
  7. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/python/openscad_cpp_evaluator/__init__.py +12 -2
  8. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/colored_body.cpp +7 -0
  9. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/export.cpp +118 -1
  10. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_export.cpp +182 -0
  11. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_python_bindings.py +40 -0
  12. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/.github/workflows/ci.yml +0 -0
  13. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/.github/workflows/wheels.yml +0 -0
  14. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/.gitignore +0 -0
  15. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/.gitmodules +0 -0
  16. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/CMakeLists.txt +0 -0
  17. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/LICENSE +0 -0
  18. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/README.md +0 -0
  19. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/bindings/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/bindings/ast_to_py.cpp +0 -0
  21. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/cmake/embed_font.cmake +0 -0
  22. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/examples/CMakeLists.txt +0 -0
  23. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/examples/eval_perf_benchmark.cpp +0 -0
  24. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/examples/manifold_cache_reuse.cpp +0 -0
  25. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/examples/minimal_debugger.cpp +0 -0
  26. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  27. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/.gitignore +0 -0
  28. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  29. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  30. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/README.md +0 -0
  31. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  32. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  33. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  34. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  35. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  36. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  37. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  38. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  39. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  40. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  41. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  42. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  43. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  44. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  45. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  46. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  47. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  48. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  49. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  50. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  51. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  52. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  53. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  54. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  55. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  56. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  57. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  58. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  59. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  60. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  61. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  62. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  63. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  64. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  65. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  66. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  67. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  68. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  69. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  70. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  71. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  72. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  73. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  74. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  75. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  76. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  77. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  78. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  79. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  80. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  81. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  82. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  83. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  84. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  85. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  86. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  87. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  88. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  89. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  90. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  91. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  92. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  93. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  94. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  95. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  96. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  97. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  98. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  99. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  100. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  101. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  102. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  103. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  104. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  105. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  106. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  107. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  108. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  109. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  110. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  111. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  112. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  113. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  114. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  115. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  116. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  117. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  118. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  119. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  120. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  121. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  122. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  123. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  124. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  125. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  126. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  127. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  128. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/resources/fonts/LICENSE +0 -0
  129. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  130. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/CMakeLists.txt +0 -0
  131. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/booleans.cpp +0 -0
  132. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/builtins.hpp +0 -0
  133. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/call_args.cpp +0 -0
  134. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/color.cpp +0 -0
  135. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/control.cpp +0 -0
  136. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/dxf_dim.cpp +0 -0
  137. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/extrude.cpp +0 -0
  138. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/function_builtins.cpp +0 -0
  139. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/import.cpp +0 -0
  140. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/modifiers.cpp +0 -0
  141. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/primitives_2d.cpp +0 -0
  142. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/primitives_3d.cpp +0 -0
  143. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/registry.cpp +0 -0
  144. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/roof.cpp +0 -0
  145. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/segments.cpp +0 -0
  146. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/surface.cpp +0 -0
  147. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/text.cpp +0 -0
  148. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/topology.cpp +0 -0
  149. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/builtins/transforms.cpp +0 -0
  150. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/bytecode_compiler.cpp +0 -0
  151. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/bytecode_vm.cpp +0 -0
  152. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/csg_generate.cpp +0 -0
  153. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/csg_resolve.cpp +0 -0
  154. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/css_colors.cpp +0 -0
  155. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/debug/debug_repl.cpp +0 -0
  156. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/debug_profile.cpp +0 -0
  157. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/eval_context.cpp +0 -0
  158. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/eval_error.cpp +0 -0
  159. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/eval_use.cpp +0 -0
  160. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/expr_eval.cpp +0 -0
  161. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/format_closure.cpp +0 -0
  162. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/import/dxf_import.cpp +0 -0
  163. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/import/mesh_import.cpp +0 -0
  164. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/import/surface_load.cpp +0 -0
  165. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/import/svg_import.cpp +0 -0
  166. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/manifold_cache.cpp +0 -0
  167. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/measure_geometry.cpp +0 -0
  168. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/mesh_check.cpp +0 -0
  169. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/stmt_eval.cpp +0 -0
  170. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/text/font_match.cpp +0 -0
  171. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/text/freetype_font_provider.cpp +0 -0
  172. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/text/text_metrics.cpp +0 -0
  173. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/user_calls.cpp +0 -0
  174. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/utf8.cpp +0 -0
  175. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/value.cpp +0 -0
  176. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/src/zip_stored.cpp +0 -0
  177. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/CMakeLists.txt +0 -0
  178. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_booleans.cpp +0 -0
  179. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_bytecode_compiler.cpp +0 -0
  180. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_children_separate.cpp +0 -0
  181. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_cli.cpp +0 -0
  182. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_control_flow.cpp +0 -0
  183. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_csg_tree.cpp +0 -0
  184. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_debug_hooks.cpp +0 -0
  185. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_dxf_svg_import.cpp +0 -0
  186. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_eval_context.cpp +0 -0
  187. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_expr_eval.cpp +0 -0
  188. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_extrude_roof.cpp +0 -0
  189. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_feature_detection.cpp +0 -0
  190. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_function_builtins.cpp +0 -0
  191. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_helpers.hpp +0 -0
  192. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_import_export.cpp +0 -0
  193. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_include_cache.cpp +0 -0
  194. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_manifold_cache.cpp +0 -0
  195. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_mesh_check.cpp +0 -0
  196. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_multi_color_merge.cpp +0 -0
  197. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_primitives.cpp +0 -0
  198. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_profiling.cpp +0 -0
  199. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_render_expr.cpp +0 -0
  200. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_scope_trail.cpp +0 -0
  201. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_scoping.cpp +0 -0
  202. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_surface.cpp +0 -0
  203. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_tail_calls.cpp +0 -0
  204. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_text.cpp +0 -0
  205. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_transforms.cpp +0 -0
  206. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_unexpected_args.cpp +0 -0
  207. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_use.cpp +0 -0
  208. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_value.cpp +0 -0
  209. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_viewport_params.cpp +0 -0
  210. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tests/test_zip_stored.cpp +0 -0
  211. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tools/cli/CMakeLists.txt +0 -0
  212. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tools/cli/cli_lib.cpp +0 -0
  213. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tools/cli/cli_lib.hpp +0 -0
  214. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/tools/cli/main.cpp +0 -0
  215. {openscad_cpp_evaluator-1.9.0 → openscad_cpp_evaluator-1.10.0}/uv.lock +0 -0
@@ -916,8 +916,8 @@ grep for `ponytail:`.
916
916
  — skip it and the mesh comes out non-manifold at the seam). Self-contained 2D geometry math here
917
917
  (`dot2`/`len2`/`norm2`, the parabola-canonical-frame rotation in `discretizeArc`) exists nowhere
918
918
  else in this codebase.
919
- - `include/openscad_cpp_evaluator/export.hpp`, `src/export.cpp` — the writers for all eight
920
- formats `exportExtensions()` lists: 3MF, AMF, STL, OBJ, OFF, PLY, VRML, X3D (3MF via
919
+ - `include/openscad_cpp_evaluator/export.hpp`, `src/export.cpp` — the writers for all nine
920
+ formats `exportExtensions()` lists: 3MF, AMF, STL, OBJ, OFF, PLY, SVG, VRML, X3D (3MF via
921
921
  `zip_stored.hpp`'s `writeDeflateZip`). **`exportExtensions()` is the one source of truth for
922
922
  which formats exist** — the Python facade and BelfrySCAD's dialog/CLI all ask it rather than
923
923
  restating the list, because three hand-written copies had already drifted far enough that `.off`
@@ -927,11 +927,22 @@ grep for `ponytail:`.
927
927
  than the face, so an object whose triangles are not all one colour is written as one volume per
928
928
  colour — per-triangle `<color>` is legal in the spec but poorly supported by the slicers that are
929
929
  the audience for it.
930
+ **`writeSvg` is the one 2D writer**, and the only one that reads `ColoredBody::section` rather
931
+ than `.body`: SVG 1.1, millimetres at 1:1, every contour of a body (holes included) a
932
+ `M .. L .. z` subpath of that body's single `<path>`. Model space is Y-up and SVG's is Y-down, so
933
+ the whole point transform is a Y negation; the page is the bounding box padded by **half the
934
+ stroke width** and rounded outward to whole millimetres, which is why the margin is not a
935
+ constant and why `stroke=false` shrinks the document. Both rules are OpenSCAD's, read from its
936
+ `export_svg.cc` — `ExportSvg.MatchesRealOpenscadByteForByte` pins the output against the real
937
+ binary's, vertex order and 6-vertex line wrap included. A 3D body in the list throws
938
+ ("Current top level object is not a 2D object"), as OpenSCAD refuses too: there is no projection
939
+ to fall back on. `ExportOptions::svg` carries OpenSCAD's five `-O export-svg/...` settings.
930
940
  `colored_body.hpp`/`.cpp`'s `toRenderableBodies()` (thin-extrudes any top-level 2D-only result to
931
- a 1e-3-unit-tall `flatPreview` Manifold) must run on a body list before handing it to any of these
932
- writers — none of them look at `ColoredBody::section` at all, only `.body`. `tools/cli/cli_lib.cpp`
933
- is the only current caller; a future GUI/renderer caller needs the same call before passing
934
- results to its own mesh consumer.
941
+ a 1-unit-tall `flatPreview` Manifold) must run on a body list before handing it to any of the
942
+ MESH writers — none of them look at `ColoredBody::section` at all, only `.body`. Not before
943
+ `writeSvg`, which needs the sections that step throws away, and which says so rather than writing
944
+ the slab's outline. `tools/cli/cli_lib.cpp` is the only current caller; a future GUI/renderer
945
+ caller needs the same call before passing results to its own mesh consumer.
935
946
  - `include/openscad_cpp_evaluator/function_builtins.hpp`, `src/builtins/function_builtins.cpp` —
936
947
  the math/string/list/type-check builtin *function* dispatch (`isBuiltinFunctionName`/
937
948
  `evalBuiltinFunction`), a completely separate namespace/table from `dispatch.hpp`'s builtin
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.9.0
3
+ Version: 1.10.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -357,7 +357,8 @@ 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, bool splitComponents) {
360
+ bool stripSlivers, bool splitComponents, bool svgFill, const std::string& svgFillColor,
361
+ bool svgStroke, const std::string& svgStrokeColor, double svgStrokeWidth) {
361
362
  std::vector<std::string> warnings;
362
363
  {
363
364
  nb::gil_scoped_release rel;
@@ -366,6 +367,11 @@ nb::list exportModelPy(const std::string& path, const Geometry& geom, const std:
366
367
  opts.asciiStl = asciiStl;
367
368
  opts.stripSlivers = stripSlivers;
368
369
  opts.splitComponents = splitComponents;
370
+ opts.svg.fill = svgFill;
371
+ opts.svg.fillColor = svgFillColor;
372
+ opts.svg.stroke = svgStroke;
373
+ opts.svg.strokeColor = svgStrokeColor;
374
+ opts.svg.strokeWidth = svgStrokeWidth;
369
375
  warnings = oscadeval::exportModel(path, geom.bodies, opts);
370
376
  }
371
377
  nb::list out;
@@ -793,14 +799,19 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
793
799
 
794
800
  m.def("export_model", &exportModelPy, nb::arg("path"), nb::arg("geometry"), nb::arg("format") = std::string(),
795
801
  nb::arg("ascii_stl") = false, nb::arg("strip_slivers") = true,
796
- nb::arg("split_components") = false,
802
+ nb::arg("split_components") = false, nb::arg("svg_fill") = false,
803
+ nb::arg("svg_fill_color") = std::string("white"), nb::arg("svg_stroke") = true,
804
+ nb::arg("svg_stroke_color") = std::string("black"), nb::arg("svg_stroke_width") = 0.35,
797
805
  "Write `geometry` to `path`, format taken from the extension unless `format` says otherwise. "
798
806
  "Returns the warnings to surface (open shells, mesh problems, slivers removed) rather than "
799
807
  "logging them. Raises RuntimeError when there is no geometry, the format is unknown, or the "
800
808
  "file cannot be opened.\n\n"
801
809
  "`split_components` gives every disconnected piece its own object in the multi-object formats "
802
810
  "(3MF, AMF, OBJ, PLY, VRML, X3D). Off by default, matching OpenSCAD, which writes one object "
803
- "per colour however many pieces it is in.");
811
+ "per colour however many pieces it is in.\n\n"
812
+ "The svg_* arguments are OpenSCAD's -O export-svg/... set and apply to .svg only. "
813
+ "svg_stroke/svg_stroke_width also pad the page, so they are not purely cosmetic. An .svg "
814
+ "export needs an all-2D model and raises otherwise, as OpenSCAD does.");
804
815
 
805
816
  m.def("export_extensions", []() { return oscadeval::exportExtensions(); },
806
817
  "The file extensions export_model understands, dot-prefixed. Read from the C++ writer table "
@@ -16,6 +16,10 @@ enum class BodyRole { Normal, Highlight, Background, ShowOnly };
16
16
  // A single piece of evaluated geometry: exactly one of `body` (3D) or
17
17
  // `section` (2D) is set, never both -- mirrors the Python reference's
18
18
  // ColoredBody (body: Optional[Manifold] XOR section: Optional[CrossSection]).
19
+ // ONE exception, and it is deliberate: toRenderableBodies() leaves the
20
+ // `section` in place on the `flatPreview` body it extrudes, so a 2D writer
21
+ // downstream still has the contours. `flatPreview` is the flag that says
22
+ // so; nothing else sets both.
19
23
  // `role`/`triColors` are later-phase fields (modifier tagging -- Phase 3;
20
24
  // multi-color CSG merges -- Phase 3/8) included now so the struct shape
21
25
  // doesn't need reworking when those land.
@@ -127,6 +127,36 @@ void writeOff(const std::string& path, const std::vector<ColoredBody>& bodies);
127
127
  // no geometry to export.
128
128
  void writeThreeMf(const std::string& path, const std::vector<ExportObject>& objects);
129
129
 
130
+ // -- 2D ------------------------------------------------------------------
131
+
132
+ // Options for the SVG writer, named and defaulted after OpenSCAD's own
133
+ // `-O export-svg/...` set. `stroke`/`strokeWidth` are not purely cosmetic:
134
+ // half the stroke width pads the page, so turning the stroke off shrinks
135
+ // the document.
136
+ struct ExportSvgOptions {
137
+ bool fill = false;
138
+ std::string fillColor = "white";
139
+ bool stroke = true;
140
+ std::string strokeColor = "black";
141
+ double strokeWidth = 0.35;
142
+ };
143
+
144
+ // SVG 1.1 of a 2D-only model, in millimetres at 1:1 -- so a print of it
145
+ // measures what the script says. Every contour of a body, holes included,
146
+ // becomes a `M .. L .. z` subpath of that body's single `<path>`.
147
+ //
148
+ // Model space is Y-up and SVG's is Y-down, so the whole point transform is
149
+ // a Y negation. The page is the bounding box padded by half the stroke
150
+ // width and rounded OUTWARD to whole millimetres, which is why the margin
151
+ // is not a constant (0 to just under 1mm per side, depending where the
152
+ // bounds fall). Both rules are OpenSCAD's, read from its export_svg.cc and
153
+ // checked against its binary.
154
+ //
155
+ // Throws std::runtime_error if any top-level body is 3D -- OpenSCAD's rule,
156
+ // and the only honest answer, since there is no projection to fall back on.
157
+ void writeSvg(const std::string& path, const std::vector<ColoredBody>& bodies,
158
+ const ExportSvgOptions& opts = {});
159
+
130
160
  // -- one entry point ------------------------------------------------------
131
161
 
132
162
  struct ExportOptions {
@@ -144,6 +174,8 @@ struct ExportOptions {
144
174
  // pieces it is in; see that function's own comment for why the default
145
175
  // moved here.
146
176
  bool splitComponents = false;
177
+ // .svg only.
178
+ ExportSvgOptions svg;
147
179
  };
148
180
 
149
181
  // 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.9.0"
7
+ version = "1.10.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -292,7 +292,10 @@ 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, split_components: bool = False) -> list:
295
+ strip_slivers: bool = True, split_components: bool = False,
296
+ svg_fill: bool = False, svg_fill_color: str = "white",
297
+ svg_stroke: bool = True, svg_stroke_color: str = "black",
298
+ svg_stroke_width: float = 0.35) -> list:
296
299
  """Write `geometry` to `path`. Returns the warnings to surface.
297
300
 
298
301
  `geometry` is the handle an Evaluator stashes on itself as
@@ -312,10 +315,17 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
312
315
 
313
316
  `split_components` gives every disconnected piece its own object in the
314
317
  multi-object formats. Off by default, which is what OpenSCAD writes.
318
+
319
+ The `svg_*` arguments are OpenSCAD's `-O export-svg/...` set and apply
320
+ to `.svg` only, which writes a 2D model at 1:1 in millimetres.
321
+ `svg_stroke`/`svg_stroke_width` also pad the page, so they are not
322
+ purely cosmetic. An `.svg` export of anything 3D raises, as OpenSCAD's
323
+ does -- there is no projection to fall back on.
315
324
  """
316
325
  try:
317
326
  return list(_ext.export_model(path, geometry, format, ascii_stl, strip_slivers,
318
- split_components))
327
+ split_components, svg_fill, svg_fill_color,
328
+ svg_stroke, svg_stroke_color, svg_stroke_width))
319
329
  except Exception as e:
320
330
  raise EvalError(str(e)) from e
321
331
 
@@ -43,6 +43,13 @@ std::vector<ColoredBody> toRenderableBodies(const std::vector<ColoredBody>& bodi
43
43
  flat.body = flat.body->Transform(cb.sectionXform);
44
44
  flat.color = cb.color;
45
45
  flat.flatPreview = true;
46
+ // Carried, not consumed: the 2D writers (writeSvg) need the
47
+ // contours, and every caller that exports goes through this
48
+ // conversion first -- the Python binding hands ONE body list to
49
+ // both the renderer and export. Dropping it here made an SVG
50
+ // export of any 2D script impossible from Python.
51
+ flat.section = cb.section;
52
+ flat.sectionXform = cb.sectionXform;
46
53
  flat.role = cb.role;
47
54
  out.push_back(std::move(flat));
48
55
  } else {
@@ -999,8 +999,117 @@ void writeOffMesh(const std::string& path, const manifold::MeshGL& mesh) {
999
999
 
1000
1000
  } // namespace
1001
1001
 
1002
+ namespace {
1003
+
1004
+ // The contours of one 2D body, projected to XY.
1005
+ //
1006
+ // A section can carry a transform a CrossSection itself cannot hold -- a Z
1007
+ // offset, a rotation out of the plane (ColoredBody::sectionXform). Applying
1008
+ // it and keeping x/y is the top view, which is the only thing a flat page
1009
+ // can show; for the overwhelmingly common case (no such transform) it is
1010
+ // the identity and the points pass through untouched.
1011
+ manifold::Polygons sectionContours(const ColoredBody& cb) {
1012
+ manifold::Polygons polys = cb.section->ToPolygons();
1013
+ const manifold::mat3x4& m = cb.sectionXform;
1014
+ for (manifold::SimplePolygon& contour : polys) {
1015
+ for (manifold::vec2& p : contour) {
1016
+ const double x = m[0].x * p.x + m[1].x * p.y + m[3].x;
1017
+ const double y = m[0].y * p.x + m[1].y * p.y + m[3].y;
1018
+ p = manifold::vec2(x, y);
1019
+ }
1020
+ }
1021
+ return polys;
1022
+ }
1023
+
1024
+ } // namespace
1025
+
1026
+ void writeSvg(const std::string& path, const std::vector<ColoredBody>& bodies, const ExportSvgOptions& opts) {
1027
+ // OpenSCAD's own wording, and its rule: 2D export needs an all-2D top
1028
+ // level. There is no sane projection to fall back on -- a silhouette
1029
+ // would be a different model from the one the script describes -- so
1030
+ // refusing is the honest answer.
1031
+ const char* kNot2d = "Current top level object is not a 2D object";
1032
+
1033
+ std::vector<manifold::Polygons> perBody;
1034
+ double minx = 0, miny = 0, maxx = 0, maxy = 0;
1035
+ bool any = false;
1036
+ for (const ColoredBody& cb : bodies) {
1037
+ if (!isExportable(cb)) continue;
1038
+ // The section decides, not the Manifold: a body that has been
1039
+ // through toRenderableBodies() carries BOTH -- a 1-unit slab
1040
+ // extruded so a 2D script has something to show, and the contours
1041
+ // it came from. Those contours are the real geometry, and the
1042
+ // binding hands export that same converted list, so reading `body`
1043
+ // first would make SVG unreachable for every 2D script.
1044
+ if (!cb.section) {
1045
+ if (cb.isDisplayOnly()) throw std::runtime_error(kNot2d);
1046
+ if (cb.body && !cb.body->IsEmpty()) throw std::runtime_error(kNot2d);
1047
+ continue;
1048
+ }
1049
+ manifold::Polygons polys = sectionContours(cb);
1050
+ for (const manifold::SimplePolygon& contour : polys) {
1051
+ for (const manifold::vec2& p : contour) {
1052
+ if (!any) {
1053
+ minx = maxx = p.x;
1054
+ miny = maxy = p.y;
1055
+ any = true;
1056
+ } else {
1057
+ minx = std::min(minx, p.x);
1058
+ maxx = std::max(maxx, p.x);
1059
+ miny = std::min(miny, p.y);
1060
+ maxy = std::max(maxy, p.y);
1061
+ }
1062
+ }
1063
+ }
1064
+ perBody.push_back(std::move(polys));
1065
+ }
1066
+ if (!any) throw std::runtime_error("No geometry to export");
1067
+
1068
+ // Page = bounding box, padded by half the stroke (the stroke straddles
1069
+ // the contour, so half of it lies outside), rounded outward to whole
1070
+ // millimetres. Note the Y terms use -maxy/-miny: the negation below
1071
+ // turns the TOP of the model into the most negative coordinate.
1072
+ const double pad = opts.stroke ? opts.strokeWidth / 2.0 : 0.0;
1073
+ const long left = static_cast<long>(std::floor(minx - pad));
1074
+ const long top = static_cast<long>(std::floor(-maxy - pad));
1075
+ const long right = static_cast<long>(std::ceil(maxx + pad));
1076
+ const long bottom = static_cast<long>(std::ceil(-miny + pad));
1077
+ const long width = right - left;
1078
+ const long height = bottom - top;
1079
+
1080
+ std::ofstream out(path);
1081
+ if (!out) throw std::runtime_error("Could not open '" + path + "' for writing");
1082
+
1083
+ const std::string stroke = opts.stroke ? opts.strokeColor : "none";
1084
+ const std::string fill = opts.fill ? opts.fillColor : "none";
1085
+
1086
+ out << "<?xml version=\"1.0\" standalone=\"no\"?>\n"
1087
+ << "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
1088
+ "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"
1089
+ << "<svg width=\"" << width << "mm\" height=\"" << height << "mm\" viewBox=\"" << left << " " << top
1090
+ << " " << width << " " << height << "\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n"
1091
+ << "<title>OpenSCAD Model</title>\n";
1092
+
1093
+ for (const manifold::Polygons& polys : perBody) {
1094
+ out << "<path d=\"\n";
1095
+ for (const manifold::SimplePolygon& contour : polys) {
1096
+ if (contour.empty()) continue;
1097
+ out << "M " << contour[0].x << "," << -contour[0].y;
1098
+ for (size_t i = 1; i < contour.size(); ++i) {
1099
+ out << " L " << contour[i].x << "," << -contour[i].y;
1100
+ if ((i % 6) == 5) out << "\n";
1101
+ }
1102
+ out << " z\n";
1103
+ }
1104
+ out << "\" stroke=\"" << stroke << "\" fill=\"" << fill << "\" stroke-width=\"" << opts.strokeWidth
1105
+ << "\"/>\n";
1106
+ }
1107
+ out << "</svg>\n";
1108
+ }
1109
+
1002
1110
  const std::vector<std::string>& exportExtensions() {
1003
- static const std::vector<std::string> exts = {".3mf", ".amf", ".stl", ".obj", ".off", ".ply", ".wrl", ".x3d"};
1111
+ static const std::vector<std::string> exts = {".3mf", ".amf", ".stl", ".obj", ".off",
1112
+ ".ply", ".svg", ".wrl", ".x3d"};
1004
1113
  return exts;
1005
1114
  }
1006
1115
 
@@ -1021,6 +1130,14 @@ std::vector<std::string> exportModel(const std::string& path, const std::vector<
1021
1130
  }
1022
1131
  };
1023
1132
 
1133
+ if (ext == ".svg") {
1134
+ // 2D, and nothing the mesh pipeline below does applies: no merge,
1135
+ // no sliver strip, no manifoldness check. Contours are what they
1136
+ // are.
1137
+ writeSvg(path, bodies, opts.svg);
1138
+ return warnings;
1139
+ }
1140
+
1024
1141
  if (isMultiObject(ext)) {
1025
1142
  // These keep the parts as separate objects, so each is checked on
1026
1143
  // its own -- that is what the file contains.
@@ -258,3 +258,185 @@ TEST(ExportAmf, AMultiColouredObjectBecomesOneVolumePerColour) {
258
258
  EXPECT_EQ(countOf(xml, "<material id="), 2u);
259
259
  std::remove(path.c_str());
260
260
  }
261
+
262
+ // -- SVG (2D) --------------------------------------------------------------
263
+
264
+ namespace {
265
+
266
+ std::string readText(const std::string& path) {
267
+ std::ifstream in(path);
268
+ return std::string((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
269
+ }
270
+
271
+ // Write `code` as SVG and return the file's text, deleting the file.
272
+ std::string svgOf(const std::string& code, const std::string& name, const ExportSvgOptions& opts = {}) {
273
+ const std::string path = tempPath(name).string();
274
+ writeSvg(path, evalToBodies(code), opts);
275
+ const std::string text = readText(path);
276
+ std::remove(path.c_str());
277
+ return text;
278
+ }
279
+
280
+ std::string firstLineContaining(const std::string& text, const std::string& needle) {
281
+ const size_t at = text.find(needle);
282
+ if (at == std::string::npos) return "";
283
+ const size_t start = text.rfind('\n', at);
284
+ const size_t end = text.find('\n', at);
285
+ return text.substr(start == std::string::npos ? 0 : start + 1,
286
+ (end == std::string::npos ? text.size() : end) - (start == std::string::npos ? 0 : start + 1));
287
+ }
288
+
289
+ } // namespace
290
+
291
+ // The page rule is NOT a fixed 1mm margin -- it is the bounding box padded
292
+ // by half the stroke width, then rounded outward to whole millimetres. The
293
+ // three cases below are the ones that tell those two rules apart, and each
294
+ // header is what real OpenSCAD 2026.02.01 wrote for the same model.
295
+ TEST(ExportSvg, PageIsStrokePaddedBoundsRoundedOutward) {
296
+ const std::string text = svgOf("square([70.4, 25.3]);", "page_default.svg");
297
+ EXPECT_EQ(firstLineContaining(text, "<svg "),
298
+ "<svg width=\"72mm\" height=\"27mm\" viewBox=\"-1 -26 72 27\" "
299
+ "xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">");
300
+ }
301
+
302
+ TEST(ExportSvg, NoStrokeMeansNoStrokePadding) {
303
+ ExportSvgOptions opts;
304
+ opts.stroke = false;
305
+ const std::string text = svgOf("square([70.4, 25.3]);", "page_nostroke.svg", opts);
306
+ EXPECT_EQ(firstLineContaining(text, "<svg "),
307
+ "<svg width=\"71mm\" height=\"26mm\" viewBox=\"0 -26 71 26\" "
308
+ "xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">");
309
+ EXPECT_NE(text.find("stroke=\"none\""), std::string::npos);
310
+ }
311
+
312
+ TEST(ExportSvg, WiderStrokeGrowsThePage) {
313
+ ExportSvgOptions opts;
314
+ opts.strokeWidth = 4.0;
315
+ const std::string text = svgOf("square([70.4, 25.3]);", "page_wide.svg", opts);
316
+ EXPECT_EQ(firstLineContaining(text, "<svg "),
317
+ "<svg width=\"75mm\" height=\"30mm\" viewBox=\"-2 -28 75 30\" "
318
+ "xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">");
319
+ }
320
+
321
+ TEST(ExportSvg, NegatesY) {
322
+ // Model Y 10..15 must come out as SVG y -15..-10: SVG's Y grows
323
+ // downward. Getting this backwards mirrors the drawing, and on a
324
+ // symmetric model it is invisible -- hence an asymmetric one.
325
+ const std::string text = svgOf("translate([0,10]) square([5,5]);", "negate_y.svg");
326
+ EXPECT_NE(text.find(",-15"), std::string::npos);
327
+ EXPECT_NE(text.find(",-10"), std::string::npos);
328
+ EXPECT_EQ(text.find(",15"), std::string::npos);
329
+ }
330
+
331
+ TEST(ExportSvg, HoleIsAnotherSubpathOfTheSamePath) {
332
+ const std::string text =
333
+ svgOf("difference() { square([40,25]); translate([20,12]) circle(5, $fn=16); }", "hole.svg");
334
+ EXPECT_EQ(countOf(text, "<path "), 1u); // one body, one <path>
335
+ EXPECT_EQ(countOf(text, "M "), 2u); // outline + hole
336
+ EXPECT_EQ(countOf(text, " z"), 2u);
337
+ // fill="none" is why the hole needs no winding rule to show through.
338
+ EXPECT_NE(text.find("fill=\"none\""), std::string::npos);
339
+ }
340
+
341
+ TEST(ExportSvg, DisjointShapesShareOneBodyAndOnePath) {
342
+ const std::string text = svgOf("square(10); translate([20,0]) square(10);", "disjoint.svg");
343
+ EXPECT_EQ(countOf(text, "M "), 2u);
344
+ // 0..30 in x, padded by 0.175 and rounded out: -1 .. 31.
345
+ EXPECT_NE(text.find("<svg width=\"32mm\" height=\"12mm\" viewBox=\"-1 -11 32 12\""), std::string::npos);
346
+ }
347
+
348
+ TEST(ExportSvg, StrokeAndFillOptionsReachTheFile) {
349
+ ExportSvgOptions opts;
350
+ opts.fill = true;
351
+ opts.fillColor = "#ffcc00";
352
+ opts.strokeColor = "red";
353
+ opts.strokeWidth = 0.5;
354
+ const std::string text = svgOf("square(10);", "styled.svg", opts);
355
+ EXPECT_NE(text.find("stroke=\"red\""), std::string::npos);
356
+ EXPECT_NE(text.find("fill=\"#ffcc00\""), std::string::npos);
357
+ EXPECT_NE(text.find("stroke-width=\"0.5\""), std::string::npos);
358
+ }
359
+
360
+ TEST(ExportSvg, SectionTransformIsApplied) {
361
+ // A transform a CrossSection cannot hold -- here a rotation out of the
362
+ // XY plane -- lives in ColoredBody::sectionXform instead. Ignoring it
363
+ // drew the unrotated shape. Tilting a 10mm square 45 degrees about X
364
+ // foreshortens it to 10*cos(45) = 7.07107 on the page.
365
+ const std::string text = svgOf("rotate([45,0,0]) square([10,10]);", "xform.svg");
366
+ EXPECT_NE(text.find("-7.07107"), std::string::npos);
367
+ EXPECT_NE(text.find("<svg width=\"12mm\" height=\"9mm\" viewBox=\"-1 -8 12 9\""), std::string::npos);
368
+ }
369
+
370
+ TEST(ExportSvg, InPlaneTranslationLandsWhereTheModelIs) {
371
+ const std::string text = svgOf("translate([3,4]) square([5,5]);", "moved.svg");
372
+ EXPECT_NE(text.find("M 3,-4"), std::string::npos);
373
+ EXPECT_NE(text.find("L 8,-9"), std::string::npos);
374
+ }
375
+
376
+ TEST(ExportSvg, RefusesA3dModel) {
377
+ std::vector<ColoredBody> bodies = evalToBodies("cube(10);");
378
+ EXPECT_THROW(writeSvg(tempPath("solid.svg").string(), bodies), std::runtime_error);
379
+ }
380
+
381
+ TEST(ExportSvg, RefusesMixed2dAnd3d) {
382
+ std::vector<ColoredBody> bodies = evalToBodies("cube(10); translate([20,0]) square(5);");
383
+ EXPECT_THROW(writeSvg(tempPath("mixed.svg").string(), bodies), std::runtime_error);
384
+ }
385
+
386
+ TEST(ExportSvg, WorksOnRenderableBodiesToo) {
387
+ // The Python binding runs toRenderableBodies() once and hands the SAME
388
+ // list to the renderer and to export, so a 2D script reaches writeSvg
389
+ // as a 1-unit slab that still carries its section. The contours are
390
+ // what gets written -- the slab's own outline would be the same shape
391
+ // but is not the point; the section is the geometry.
392
+ std::vector<ColoredBody> renderable = toRenderableBodies(evalToBodies("square([70.4, 25.3]);"));
393
+ ASSERT_TRUE(renderable[0].flatPreview);
394
+ ASSERT_TRUE(renderable[0].section.has_value());
395
+ const std::string path = tempPath("preview.svg").string();
396
+ writeSvg(path, renderable);
397
+ const std::string text = readText(path);
398
+ std::remove(path.c_str());
399
+ EXPECT_NE(text.find("<svg width=\"72mm\" height=\"27mm\" viewBox=\"-1 -26 72 27\""), std::string::npos);
400
+ EXPECT_EQ(countOf(text, "M "), 1u);
401
+ }
402
+
403
+ TEST(ExportSvg, NoGeometryThrows) {
404
+ std::vector<ColoredBody> empty;
405
+ EXPECT_THROW(writeSvg(tempPath("empty.svg").string(), empty), std::runtime_error);
406
+ }
407
+
408
+ TEST(ExportSvg, ReachableThroughExportModelAndListedAsAnExtension) {
409
+ const std::vector<std::string>& exts = exportExtensions();
410
+ EXPECT_NE(std::find(exts.begin(), exts.end(), ".svg"), exts.end());
411
+
412
+ const std::string path = tempPath("via_export_model.svg").string();
413
+ const std::vector<std::string> warnings = exportModel(path, evalToBodies("circle(5, $fn=8);"), ExportOptions{});
414
+ EXPECT_TRUE(warnings.empty()); // none of the mesh checks apply to contours
415
+ const std::string text = readText(path);
416
+ EXPECT_NE(text.find("<svg "), std::string::npos);
417
+ EXPECT_EQ(countOf(text, "M "), 1u);
418
+ std::remove(path.c_str());
419
+ }
420
+
421
+ // Byte-for-byte against real OpenSCAD 2026.02.01's own `-o out.svg` for the
422
+ // same script -- header, vertex order, the 6-vertex line wrap and the "-0"
423
+ // that a negated zero prints as. Contour ORDER (outline before hole) and
424
+ // winding come from Manifold rather than CGAL, so agreeing here is worth
425
+ // pinning: it is the part that could drift without anything else noticing.
426
+ TEST(ExportSvg, MatchesRealOpenscadByteForByte) {
427
+ const std::string expected =
428
+ R"SVG(<?xml version="1.0" standalone="no"?>
429
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
430
+ <svg width="42mm" height="27mm" viewBox="-1 -26 42 27" xmlns="http://www.w3.org/2000/svg" version="1.1">
431
+ <title>OpenSCAD Model</title>
432
+ <path d="
433
+ M 40,-25 L 0,-25 L 0,-0 L 40,-0 z
434
+ M 18.0866,-7.3806 L 16.4645,-8.46447 L 15.3806,-10.0866 L 15,-12 L 15.3806,-13.9134 L 16.4645,-15.5355
435
+ L 18.0866,-16.6194 L 20,-17 L 21.9134,-16.6194 L 23.5355,-15.5355 L 24.6194,-13.9134 L 25,-12
436
+ L 24.6194,-10.0866 L 23.5355,-8.46447 L 21.9134,-7.3806 L 20,-7 z
437
+ " stroke="black" fill="none" stroke-width="0.35"/>
438
+ </svg>
439
+ )SVG";
440
+ EXPECT_EQ(svgOf("difference() { square([40,25]); translate([20,12]) circle(5, $fn=16); }", "parity.svg"),
441
+ expected);
442
+ }
@@ -661,6 +661,46 @@ def test_export_model_facade_accepts_split_components(tmp_path):
661
661
  assert objects_in(tmp_path / "split.3mf", split_components=True) == 3
662
662
 
663
663
 
664
+ def test_export_model_writes_svg_and_takes_its_options(tmp_path):
665
+ """.svg through the facade, options included -- the same two-parameter-list
666
+ trap as test_export_model_facade_accepts_split_components."""
667
+ from openscad_cpp_evaluator import Evaluator, EvalError, export_extensions, export_model
668
+
669
+ assert ".svg" in export_extensions()
670
+
671
+ src = tmp_path / "flat.scad"
672
+ src.write_text("square([70.4, 25.3]);")
673
+ ev = Evaluator()
674
+ ev.evaluate(str(src), {})
675
+
676
+ out = tmp_path / "flat.svg"
677
+ assert export_model(str(out), ev.geometry) == []
678
+ text = out.read_text()
679
+ # Page = bounds + half the stroke, rounded outward; matches OpenSCAD's.
680
+ assert 'width="72mm" height="27mm" viewBox="-1 -26 72 27"' in text
681
+ assert 'stroke="black" fill="none" stroke-width="0.35"' in text
682
+
683
+ styled = tmp_path / "styled.svg"
684
+ export_model(str(styled), ev.geometry, svg_stroke=False, svg_fill=True, svg_fill_color="red")
685
+ styled_text = styled.read_text()
686
+ assert 'stroke="none" fill="red"' in styled_text
687
+ # No stroke, no stroke padding -- so the page shrinks.
688
+ assert 'width="71mm" height="26mm"' in styled_text
689
+
690
+
691
+ def test_export_model_svg_refuses_a_3d_model(tmp_path):
692
+ """OpenSCAD's rule, and ours: 2D export needs an all-2D top level."""
693
+ import pytest
694
+ from openscad_cpp_evaluator import Evaluator, EvalError, export_model
695
+
696
+ src = tmp_path / "solid.scad"
697
+ src.write_text("cube(10);")
698
+ ev = Evaluator()
699
+ ev.evaluate(str(src), {})
700
+ with pytest.raises(EvalError, match="not a 2D object"):
701
+ export_model(str(tmp_path / "solid.svg"), ev.geometry)
702
+
703
+
664
704
  def test_strict_commas_reaches_the_parser(tmp_path):
665
705
  """Evaluator.evaluate(strict_commas=True) must survive both hand-written
666
706
  parameter lists -- the facade's and the nanobind binding's. The last