openscad-cpp-evaluator 0.35.0__tar.gz → 0.36.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 (203) hide show
  1. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/CLAUDE.md +23 -0
  2. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/primitives_3d.cpp +241 -38
  5. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/registry.cpp +1 -1
  6. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_primitives.cpp +126 -0
  7. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/uv.lock +1 -1
  8. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/.github/workflows/ci.yml +0 -0
  9. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/.github/workflows/wheels.yml +0 -0
  10. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/.gitignore +0 -0
  11. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/.gitmodules +0 -0
  12. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/CMakeLists.txt +0 -0
  13. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/LICENSE +0 -0
  14. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/README.md +0 -0
  15. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/bindings/CMakeLists.txt +0 -0
  16. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/bindings/ast_to_py.cpp +0 -0
  17. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/bindings/module.cpp +0 -0
  18. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/cmake/embed_font.cmake +0 -0
  19. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/examples/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/examples/eval_perf_benchmark.cpp +0 -0
  21. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/examples/manifold_cache_reuse.cpp +0 -0
  22. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/examples/minimal_debugger.cpp +0 -0
  23. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  24. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/.gitignore +0 -0
  25. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  26. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  27. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/README.md +0 -0
  28. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  29. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  30. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  31. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  32. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  33. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  34. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  35. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  36. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  37. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  38. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  39. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  40. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  41. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  42. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  43. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  44. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  45. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  46. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  47. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  48. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  49. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  50. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  51. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  52. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  53. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  54. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  55. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  56. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  57. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  58. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  59. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  60. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  61. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  62. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  63. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  64. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  65. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  66. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  67. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  68. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  69. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  70. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  71. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  72. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  73. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  74. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  75. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  76. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  77. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  78. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  79. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  80. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  81. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  82. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  83. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  84. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  85. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  86. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  87. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  88. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  89. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  90. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  91. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  92. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  93. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  94. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  95. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  96. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  97. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  98. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  99. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  100. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  101. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  102. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  103. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  104. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  105. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  106. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  107. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  108. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  109. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  110. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  111. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  112. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  113. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  114. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  115. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  116. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  117. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  118. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  119. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  120. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  121. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  122. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  123. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/resources/fonts/LICENSE +0 -0
  124. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  125. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/CMakeLists.txt +0 -0
  126. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/booleans.cpp +0 -0
  127. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/builtins.hpp +0 -0
  128. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/call_args.cpp +0 -0
  129. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/color.cpp +0 -0
  130. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/control.cpp +0 -0
  131. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/dxf_dim.cpp +0 -0
  132. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/extrude.cpp +0 -0
  133. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/function_builtins.cpp +0 -0
  134. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/import.cpp +0 -0
  135. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/modifiers.cpp +0 -0
  136. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/primitives_2d.cpp +0 -0
  137. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/roof.cpp +0 -0
  138. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/segments.cpp +0 -0
  139. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/surface.cpp +0 -0
  140. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/text.cpp +0 -0
  141. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/topology.cpp +0 -0
  142. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/builtins/transforms.cpp +0 -0
  143. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/bytecode_compiler.cpp +0 -0
  144. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/bytecode_vm.cpp +0 -0
  145. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/colored_body.cpp +0 -0
  146. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/csg_generate.cpp +0 -0
  147. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/csg_resolve.cpp +0 -0
  148. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/css_colors.cpp +0 -0
  149. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/debug/debug_repl.cpp +0 -0
  150. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/debug_profile.cpp +0 -0
  151. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/eval_context.cpp +0 -0
  152. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/eval_error.cpp +0 -0
  153. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/eval_use.cpp +0 -0
  154. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/export.cpp +0 -0
  155. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/expr_eval.cpp +0 -0
  156. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/import/dxf_import.cpp +0 -0
  157. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/import/mesh_import.cpp +0 -0
  158. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/import/surface_load.cpp +0 -0
  159. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/import/svg_import.cpp +0 -0
  160. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/manifold_cache.cpp +0 -0
  161. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/mesh_check.cpp +0 -0
  162. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/stmt_eval.cpp +0 -0
  163. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/text/font_match.cpp +0 -0
  164. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/text/freetype_font_provider.cpp +0 -0
  165. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/text/text_metrics.cpp +0 -0
  166. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/user_calls.cpp +0 -0
  167. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/value.cpp +0 -0
  168. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/src/zip_stored.cpp +0 -0
  169. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/CMakeLists.txt +0 -0
  170. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_booleans.cpp +0 -0
  171. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_bytecode_compiler.cpp +0 -0
  172. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_cli.cpp +0 -0
  173. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_control_flow.cpp +0 -0
  174. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_csg_tree.cpp +0 -0
  175. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_debug_hooks.cpp +0 -0
  176. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_dxf_svg_import.cpp +0 -0
  177. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_eval_context.cpp +0 -0
  178. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_export.cpp +0 -0
  179. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_expr_eval.cpp +0 -0
  180. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_extrude_roof.cpp +0 -0
  181. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_function_builtins.cpp +0 -0
  182. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_helpers.hpp +0 -0
  183. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_import_export.cpp +0 -0
  184. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_manifold_cache.cpp +0 -0
  185. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_mesh_check.cpp +0 -0
  186. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_multi_color_merge.cpp +0 -0
  187. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_profiling.cpp +0 -0
  188. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_python_bindings.py +0 -0
  189. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_scope_trail.cpp +0 -0
  190. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_scoping.cpp +0 -0
  191. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_surface.cpp +0 -0
  192. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_tail_calls.cpp +0 -0
  193. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_text.cpp +0 -0
  194. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_transforms.cpp +0 -0
  195. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_unexpected_args.cpp +0 -0
  196. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_use.cpp +0 -0
  197. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_value.cpp +0 -0
  198. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_viewport_params.cpp +0 -0
  199. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tests/test_zip_stored.cpp +0 -0
  200. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tools/cli/CMakeLists.txt +0 -0
  201. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tools/cli/cli_lib.cpp +0 -0
  202. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tools/cli/cli_lib.hpp +0 -0
  203. {openscad_cpp_evaluator-0.35.0 → openscad_cpp_evaluator-0.36.0}/tools/cli/main.cpp +0 -0
@@ -561,6 +561,29 @@ grep for `ponytail:`.
561
561
  color instead of preserving each part's own, matching real OpenSCAD's actual behavior once fixed
562
562
  (cross-checked triangle-color-count-and-values against the Python reference directly, not just
563
563
  “does it run”).
564
+ - `src/builtins/primitives_3d.cpp` — `sphere(style=)` is a BelfrySCAD extension naming the
565
+ tessellation, using BOSL2's five `spheroid()` style names and constructions: `"orig"` (the default,
566
+ and what OpenSCAD's own sphere() builds — rings offset half a step from the poles, no pole vertex),
567
+ `"aligned"` (a vertex at each pole, so an `$fn` divisible by 4 also lands vertices exactly on ±X
568
+ and ±Y), `"stagger"` (aligned with alternate rings rotated half a face), `"octa"` and `"icosa"`.
569
+ Only `"orig"` renders the same in real OpenSCAD.
570
+
571
+ `"octa"` is `Manifold::Sphere`, which already *is* a subdivided octahedron (`Shape::Octahedron`
572
+ then `Subdivide`). Same 146/288 topology as BOSL2's at `$fn=24`, ~0.05% apart in volume because
573
+ Manifold distributes the subdivision differently. `"icosa"` samples each icosahedral face against
574
+ its own corners rather than rotating copies of one sampled face the way BOSL2 does — the sampling
575
+ is affine in those corners, so it is the same points with far less machinery — and welds the
576
+ shared edges itself so the mesh reaches Manifold already manifold.
577
+
578
+ **BOSL2's VNF winding is the opposite of Manifold's**, so `buildAligned` emits every triangle with
579
+ its last two indices swapped. Without it the solid is inside-out, which `Manifold::Volume()`
580
+ reports as a NEGATIVE volume — and which comparing `|volume|` (as an STL-based check naturally
581
+ does) hides completely. Two of the tests assert a positive volume specifically to catch this.
582
+
583
+ Note when checking any of this against BOSL2: **BOSL2 shadows `sphere()`** with its own module, so
584
+ a file that includes BOSL2 to "compare against the reference" is comparing BOSL2 with itself. Call
585
+ `spheroid()` from a BOSL2-including file and `sphere()` from one without.
586
+
564
587
  - `src/builtins/dxf_dim.cpp` — `dxf_dim()`/`dxf_cross()`, which read a measurement out of a DXF file
565
588
  rather than out of the model. Ported from the reference's `io/dxfdim.cc` plus the DIMENSION/LINE
566
589
  half of `io/DxfData.cc`: `dxf_dim()` returns the first matching DIMENSION's value by its type
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.35.0
3
+ Version: 0.36.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.35.0"
7
+ version = "0.36.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -5,10 +5,12 @@
5
5
  #include "openscad_cpp_evaluator/segments.hpp"
6
6
 
7
7
  #include <algorithm>
8
+ #include <array>
8
9
  #include <cmath>
9
10
  #include <cstdint>
10
11
  #include <map>
11
12
  #include <numbers>
13
+ #include <set>
12
14
 
13
15
  namespace oscadeval {
14
16
 
@@ -100,22 +102,48 @@ std::vector<ColoredBody> generateCube(Evaluator& ev, const CSGParams& params, co
100
102
  // _resolve_sphere/_generate_sphere exactly, including the "stacks =
101
103
  // max(2, ceil(n/2))" ring count and the fan-cap winding.
102
104
 
103
- CSGParams resolveSphere(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
104
- auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
105
- Value dArg = getArg(args, std::nullopt, "d", Value{});
106
- double r;
107
- if (!isUndef(dArg)) {
108
- r = toDoubleLenient(dArg) / 2.0;
109
- } else {
110
- Value rArg = getArg(args, 0, "r", Value{});
111
- r = isUndef(rArg) ? 1.0 : toDoubleLenient(rArg);
112
- }
105
+ // sphere(r|d, style) -- `style` is a BelfrySCAD extension naming the
106
+ // tessellation, with the same five names and the same constructions as
107
+ // BOSL2's spheroid():
108
+ //
109
+ // "orig" stacked rings offset half a step from the poles, no pole
110
+ // vertex. What OpenSCAD's own sphere() builds, hence the name,
111
+ // and still the default here.
112
+ // "aligned" a vertex at each pole and rings on the latitudes between, so
113
+ // an $fn divisible by 4 puts vertices exactly on +-X and +-Y.
114
+ // "stagger" "aligned" with alternate rings rotated half a face, giving
115
+ // triangles that alternate direction rather than stacking.
116
+ // "octa" a subdivided octahedron projected onto the sphere.
117
+ // "icosa" a subdivided icosahedron projected onto the sphere -- the most
118
+ // uniform of the five.
119
+ //
120
+ // Only "orig" matches real OpenSCAD; the rest are additions, so a script
121
+ // using them will not render the same shape elsewhere.
113
122
 
114
- const int n = fnSegmentsFromCtx(effCtx, r); // longitude segments
115
- const int stacks = std::max(2, static_cast<int>(std::ceil(n / 2.0)));
116
- const double step = std::numbers::pi / stacks;
123
+ namespace {
117
124
 
118
- std::vector<double> verts; // flat x,y,z,...
125
+ // BOSL2's spherical_to_xyz(r, theta, phi): theta around Z from +X, phi down
126
+ // from +Z.
127
+ std::array<double, 3> sphericalToXyz(double r, double thetaDeg, double phiDeg) {
128
+ const double th = thetaDeg * std::numbers::pi / 180.0;
129
+ const double ph = phiDeg * std::numbers::pi / 180.0;
130
+ return {r * std::sin(ph) * std::cos(th), r * std::sin(ph) * std::sin(th), r * std::cos(ph)};
131
+ }
132
+
133
+ struct SphereMesh {
134
+ std::vector<double> verts; // flat x,y,z
135
+ std::vector<int> tris;
136
+ };
137
+
138
+ void pushVert(SphereMesh& m, const std::array<double, 3>& p) {
139
+ m.verts.insert(m.verts.end(), {p[0], p[1], p[2]});
140
+ }
141
+
142
+ // "orig": rings at (i+0.5) steps, no pole vertices, capped by a fan at each
143
+ // end. Unchanged from the original implementation.
144
+ SphereMesh buildOrig(double r, int n, int stacks) {
145
+ SphereMesh m;
146
+ const double step = std::numbers::pi / stacks;
119
147
  std::vector<std::vector<int>> rings(static_cast<size_t>(stacks));
120
148
  for (int s = 0; s < stacks; ++s) {
121
149
  const double lat = -std::numbers::pi / 2.0 + (s + 0.5) * step;
@@ -123,44 +151,214 @@ CSGParams resolveSphere(Evaluator& ev, const oscad::ModularCall& node, EvalConte
123
151
  const double z = r * std::sin(lat);
124
152
  for (int seg = 0; seg < n; ++seg) {
125
153
  const double angle = 2.0 * std::numbers::pi * seg / n;
126
- rings[static_cast<size_t>(s)].push_back(static_cast<int>(verts.size() / 3));
127
- verts.push_back(ringR * std::cos(angle));
128
- verts.push_back(ringR * std::sin(angle));
129
- verts.push_back(z);
154
+ rings[static_cast<size_t>(s)].push_back(static_cast<int>(m.verts.size() / 3));
155
+ pushVert(m, {ringR * std::cos(angle), ringR * std::sin(angle), z});
130
156
  }
131
157
  }
132
-
133
- std::vector<int> tris;
134
158
  const auto& bot = rings.front();
135
- for (int i = 1; i < n - 1; ++i) {
136
- tris.insert(tris.end(), {bot[0], bot[static_cast<size_t>(i) + 1], bot[static_cast<size_t>(i)]});
137
- }
159
+ for (int i = 1; i < n - 1; ++i) m.tris.insert(m.tris.end(), {bot[0], bot[size_t(i) + 1], bot[size_t(i)]});
138
160
  for (int s = 0; s < stacks - 1; ++s) {
139
- const auto& lo = rings[static_cast<size_t>(s)];
140
- const auto& hi = rings[static_cast<size_t>(s) + 1];
161
+ const auto& lo = rings[size_t(s)];
162
+ const auto& hi = rings[size_t(s) + 1];
141
163
  for (int seg = 0; seg < n; ++seg) {
142
- const int a = lo[static_cast<size_t>(seg)], b = lo[static_cast<size_t>((seg + 1) % n)];
143
- const int c = hi[static_cast<size_t>(seg)], d = hi[static_cast<size_t>((seg + 1) % n)];
144
- tris.insert(tris.end(), {a, b, d});
145
- tris.insert(tris.end(), {a, d, c});
164
+ const int a = lo[size_t(seg)], b = lo[size_t((seg + 1) % n)];
165
+ const int c = hi[size_t(seg)], d = hi[size_t((seg + 1) % n)];
166
+ m.tris.insert(m.tris.end(), {a, b, d});
167
+ m.tris.insert(m.tris.end(), {a, d, c});
146
168
  }
147
169
  }
148
170
  const auto& top = rings.back();
149
- for (int i = 1; i < n - 1; ++i) {
150
- tris.insert(tris.end(), {top[0], top[static_cast<size_t>(i)], top[static_cast<size_t>(i) + 1]});
171
+ for (int i = 1; i < n - 1; ++i) m.tris.insert(m.tris.end(), {top[0], top[size_t(i)], top[size_t(i) + 1]});
172
+ return m;
173
+ }
174
+
175
+ // "aligned"/"stagger": pole, vsides-1 rings, pole. Vertex order and face
176
+ // indices follow BOSL2's spheroid() exactly so the two agree triangle for
177
+ // triangle, not merely in shape.
178
+ SphereMesh buildAligned(double r, int hsides, int vsides, bool stagger) {
179
+ SphereMesh m;
180
+ pushVert(m, sphericalToXyz(r, 0, 0)); // north pole, index 0
181
+ for (int i = 1; i <= vsides - 1; ++i) {
182
+ const double phi = i * 180.0 / vsides;
183
+ for (int j = 0; j < hsides; ++j) {
184
+ const double theta = (j + ((stagger && i % 2 != 0) ? 0.5 : 0.0)) * 360.0 / hsides;
185
+ pushVert(m, sphericalToXyz(r, theta, phi));
186
+ }
187
+ }
188
+ pushVert(m, sphericalToXyz(r, 0, 180)); // south pole, last index
189
+ const int lv = static_cast<int>(m.verts.size() / 3);
190
+
191
+ // BOSL2's VNF winding is the opposite of what Manifold wants, so every
192
+ // triangle below is emitted with its last two indices swapped. Without
193
+ // that the solid comes out inside-out -- and invisibly so if you only
194
+ // ever check |volume|, which is how this first went unnoticed.
195
+ const auto tri = [&m](int a, int b, int c) { m.tris.insert(m.tris.end(), {a, c, b}); };
196
+
197
+ for (int i = 0; i < hsides; ++i) {
198
+ const int b2 = lv - 2 - hsides;
199
+ tri(i + 1, 0, ((i + 1) % hsides) + 1);
200
+ tri(lv - 1, b2 + i + 1, b2 + ((i + 1) % hsides) + 1);
151
201
  }
202
+ for (int i = 0; i <= vsides - 3; ++i) {
203
+ const int base = 1 + hsides * i;
204
+ for (int j = 0; j < hsides; ++j) {
205
+ if (stagger && i % 2 != 0) {
206
+ tri(base + j, base + hsides + j % hsides, base + hsides + (j + hsides - 1) % hsides);
207
+ tri(base + j, base + (j + 1) % hsides, base + hsides + j);
208
+ } else {
209
+ tri(base + j, base + (j + 1) % hsides, base + hsides + (j + 1) % hsides);
210
+ tri(base + j, base + hsides + (j + 1) % hsides, base + hsides + j);
211
+ }
212
+ }
213
+ }
214
+ return m;
215
+ }
152
216
 
153
- std::vector<Value> vertsValues;
154
- vertsValues.reserve(verts.size());
155
- for (double v : verts) vertsValues.push_back(Value{v});
156
- std::vector<Value> trisValues;
157
- trisValues.reserve(tris.size());
158
- for (int t : tris) trisValues.push_back(Value{static_cast<double>(t)});
217
+ // "icosa": subdivide every icosahedral face into a triangular grid and push
218
+ // each sample out to the sphere. BOSL2 subsamples one face and rotates
219
+ // copies onto the rest; sampling each face against its own corners is the
220
+ // same points (the sampling is affine in those corners) with far less
221
+ // machinery. Coincident vertices along shared edges are welded here rather
222
+ // than left for Manifold, so the mesh arrives already manifold.
223
+ SphereMesh buildIcosa(double r, int hsides) {
224
+ const double phi = (1.0 + std::sqrt(5.0)) / 2.0;
225
+ std::vector<std::array<double, 3>> ico;
226
+ for (int i : {-1, 1}) {
227
+ for (int j : {-1, 1}) {
228
+ ico.push_back({0.0, double(i), double(j) * phi});
229
+ ico.push_back({double(i), double(j) * phi, 0.0});
230
+ ico.push_back({double(j) * phi, 0.0, double(i)});
231
+ }
232
+ }
233
+ // Hull faces by brute force: a triple is a face when every other vertex
234
+ // lies on one side of its plane. 12 vertices, so 220 triples.
235
+ std::vector<std::array<int, 3>> faces;
236
+ const int nv = static_cast<int>(ico.size());
237
+ for (int a = 0; a < nv; ++a)
238
+ for (int b = a + 1; b < nv; ++b)
239
+ for (int c = b + 1; c < nv; ++c) {
240
+ const std::array<double, 3> u{ico[b][0] - ico[a][0], ico[b][1] - ico[a][1], ico[b][2] - ico[a][2]};
241
+ const std::array<double, 3> v{ico[c][0] - ico[a][0], ico[c][1] - ico[a][1], ico[c][2] - ico[a][2]};
242
+ const std::array<double, 3> nrm{u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2],
243
+ u[0] * v[1] - u[1] * v[0]};
244
+ int pos = 0, neg = 0;
245
+ for (int k = 0; k < nv; ++k) {
246
+ if (k == a || k == b || k == c) continue;
247
+ const double d = nrm[0] * (ico[k][0] - ico[a][0]) + nrm[1] * (ico[k][1] - ico[a][1]) +
248
+ nrm[2] * (ico[k][2] - ico[a][2]);
249
+ if (d > 1e-9) ++pos;
250
+ if (d < -1e-9) ++neg;
251
+ }
252
+ if (pos && neg) continue;
253
+ // Orient outward: the normal must point away from the centre.
254
+ if (neg == 0) faces.push_back({a, c, b});
255
+ else faces.push_back({a, b, c});
256
+ }
257
+
258
+ const int steps = std::max(1, static_cast<int>(std::lround(std::max(5, hsides) / 5.0)));
259
+ const int N = steps - 1; // BOSL2's N; the grid has N+2 rows
260
+
261
+ SphereMesh m;
262
+ std::map<std::array<long long, 3>, int> weld;
263
+ const auto add = [&](const std::array<double, 3>& p) {
264
+ const double len = std::sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
265
+ const std::array<double, 3> u{r * p[0] / len, r * p[1] / len, r * p[2] / len};
266
+ const std::array<long long, 3> key{std::llround(u[0] * 1e9), std::llround(u[1] * 1e9),
267
+ std::llround(u[2] * 1e9)};
268
+ auto it = weld.find(key);
269
+ if (it != weld.end()) return it->second;
270
+ const int idx = static_cast<int>(m.verts.size() / 3);
271
+ pushVert(m, u);
272
+ weld.emplace(key, idx);
273
+ return idx;
274
+ };
275
+
276
+ for (const std::array<int, 3>& f : faces) {
277
+ const std::array<double, 3>& p0 = ico[f[0]];
278
+ const std::array<double, 3>& p1 = ico[f[1]];
279
+ const std::array<double, 3>& p2 = ico[f[2]];
280
+ // Row i has N+2-i samples, mirroring _subsample_triangle.
281
+ std::vector<std::vector<int>> grid;
282
+ for (int i = 0; i <= N + 1; ++i) {
283
+ std::vector<int> row;
284
+ for (int j = 0; j <= N + 1 - i; ++j) {
285
+ const double a = double(i) / (N + 1), b = double(j) / (N + 1);
286
+ row.push_back(add({p0[0] + (p1[0] - p0[0]) * a + (p2[0] - p0[0]) * b,
287
+ p0[1] + (p1[1] - p0[1]) * a + (p2[1] - p0[1]) * b,
288
+ p0[2] + (p1[2] - p0[2]) * a + (p2[2] - p0[2]) * b}));
289
+ }
290
+ grid.push_back(std::move(row));
291
+ }
292
+ for (int i = 0; i <= N; ++i) {
293
+ for (int j = 0; j <= N - i; ++j) {
294
+ m.tris.insert(m.tris.end(), {grid[i][j], grid[i + 1][j], grid[i][j + 1]});
295
+ if (j < N - i) {
296
+ m.tris.insert(m.tris.end(), {grid[i + 1][j], grid[i + 1][j + 1], grid[i][j + 1]});
297
+ }
298
+ }
299
+ }
300
+ }
301
+ return m;
302
+ }
303
+
304
+ } // namespace
305
+
306
+ CSGParams resolveSphere(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
307
+ auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
308
+ Value dArg = getArg(args, std::nullopt, "d", Value{});
309
+ double r;
310
+ if (!isUndef(dArg)) {
311
+ r = toDoubleLenient(dArg) / 2.0;
312
+ } else {
313
+ Value rArg = getArg(args, 0, "r", Value{});
314
+ r = isUndef(rArg) ? 1.0 : toDoubleLenient(rArg);
315
+ }
316
+
317
+ const Value styleArg = getArg(args, std::nullopt, "style", Value{});
318
+ std::string style = "orig";
319
+ if (const std::string* sv = std::get_if<std::string>(&styleArg)) {
320
+ static const std::set<std::string> known{"orig", "aligned", "stagger", "octa", "icosa"};
321
+ if (known.count(*sv)) {
322
+ style = *sv;
323
+ } else {
324
+ ev.warn("sphere: unknown style \"" + *sv + "\"; expected one of orig, aligned, stagger, octa, icosa",
325
+ &node.position());
326
+ }
327
+ } else if (!isUndef(styleArg)) {
328
+ ev.warn("sphere: style must be a string", &node.position());
329
+ }
330
+
331
+ const int hsides = fnSegmentsFromCtx(effCtx, r);
332
+ const int vsides = std::max(2, static_cast<int>(std::ceil(hsides / 2.0)));
159
333
 
160
334
  CSGParams params;
335
+ params["style"] = Value{style};
336
+ params["r"] = Value{r};
337
+ params["segs"] = Value{static_cast<double>(hsides)};
338
+ params["color"] = colorToValue(effCtx.color);
339
+
340
+ if (style == "octa") {
341
+ // Manifold::Sphere IS a subdivided octahedron (Shape::Octahedron,
342
+ // then Subdivide), so this needs no mesh of our own -- generate
343
+ // calls it directly.
344
+ params["verts"] = Value{std::make_shared<const ValueList>(ValueList{})};
345
+ params["tris"] = Value{std::make_shared<const ValueList>(ValueList{})};
346
+ return params;
347
+ }
348
+
349
+ const SphereMesh m = style == "aligned" ? buildAligned(r, hsides, vsides, false)
350
+ : style == "stagger" ? buildAligned(r, hsides, vsides, true)
351
+ : style == "icosa" ? buildIcosa(r, hsides)
352
+ : buildOrig(r, hsides, vsides);
353
+
354
+ std::vector<Value> vertsValues;
355
+ vertsValues.reserve(m.verts.size());
356
+ for (double v : m.verts) vertsValues.push_back(Value{v});
357
+ std::vector<Value> trisValues;
358
+ trisValues.reserve(m.tris.size());
359
+ for (int t : m.tris) trisValues.push_back(Value{static_cast<double>(t)});
161
360
  params["verts"] = Value{std::make_shared<const ValueList>(ValueList{std::move(vertsValues)})};
162
361
  params["tris"] = Value{std::make_shared<const ValueList>(ValueList{std::move(trisValues)})};
163
- params["color"] = colorToValue(effCtx.color);
164
362
  return params;
165
363
  }
166
364
 
@@ -174,6 +372,11 @@ std::vector<ColoredBody> generateSphere(Evaluator& ev, const CSGParams& params,
174
372
  for (const Value& t : std::get<ListPtr>(params.at("tris"))->items) {
175
373
  mesh.triVerts.push_back(static_cast<uint64_t>(std::get<double>(t)));
176
374
  }
375
+ if (std::get<std::string>(params.at("style")) == "octa") {
376
+ manifold::Manifold octa = manifold::Manifold::Sphere(
377
+ std::get<double>(params.at("r")), static_cast<int>(std::get<double>(params.at("segs"))));
378
+ return {ev.tagGenerated(std::move(octa), node, params.at("color"))};
379
+ }
177
380
  manifold::Manifold body(mesh);
178
381
  return {ev.tagGenerated(std::move(body), node, params.at("color"))};
179
382
  }
@@ -92,7 +92,7 @@ const std::vector<std::string>* builtinParamNames(const std::string& name) {
92
92
  static const std::unordered_map<std::string, std::vector<std::string>> table = {
93
93
  // -- builtin modules ---------------------------------------------
94
94
  {"cube", {"size", "center"}},
95
- {"sphere", {"r", "d"}},
95
+ {"sphere", {"r", "d", "style"}},
96
96
  {"cylinder", {"h", "r1", "r2", "center", "r", "d", "d1", "d2"}},
97
97
  {"polyhedron", {"points", "faces", "convexity", "triangles"}},
98
98
  {"square", {"size", "center"}},
@@ -273,3 +273,129 @@ TEST(Polyhedron, OpenMeshIsExcludedFromCsgButSurvivesIt) {
273
273
  EXPECT_EQ(solids, 1);
274
274
  EXPECT_EQ(displayOnly, 1);
275
275
  }
276
+
277
+ // -- sphere(style=) -------------------------------------------------------
278
+ //
279
+ // A BelfrySCAD extension naming the tessellation, with BOSL2's five style
280
+ // names and constructions. Every vertex/triangle count below was compared
281
+ // against BOSL2's own spheroid(r=10, style=..., circum=false, $fn=24), and
282
+ // the volumes matched exactly for aligned, stagger and icosa.
283
+ //
284
+ // Note BOSL2 *shadows* sphere() with its own module, so any comparison has
285
+ // to call spheroid() from a BOSL2-including file and sphere() from one
286
+ // without -- including BOSL2 to "check the reference" silently compares
287
+ // BOSL2 against itself, which is how the first run of this looked correct
288
+ // while testing nothing.
289
+
290
+ namespace {
291
+
292
+ struct SphereStats {
293
+ size_t verts = 0;
294
+ size_t tris = 0;
295
+ double volume = 0.0;
296
+ };
297
+
298
+ SphereStats sphereStats(const std::string& code) {
299
+ Evaluated e = evalSrc(code);
300
+ EXPECT_EQ(e.bodies.size(), 1u);
301
+ const manifold::Manifold& m = *e.bodies[0].body;
302
+ return {static_cast<size_t>(m.NumVert()), static_cast<size_t>(m.NumTri()), m.Volume()};
303
+ }
304
+
305
+ } // namespace
306
+
307
+ TEST(SphereStyle, DefaultsToOrigAndIsUnchanged) {
308
+ const SphereStats def = sphereStats("sphere(r=10, $fn=24);");
309
+ const SphereStats orig = sphereStats("sphere(r=10, style=\"orig\", $fn=24);");
310
+ EXPECT_EQ(def.verts, orig.verts);
311
+ EXPECT_EQ(def.tris, orig.tris);
312
+ EXPECT_NEAR(def.volume, orig.volume, 1e-9);
313
+ // The pre-existing OpenSCAD construction: 24 segments x 12 stacks, no
314
+ // pole vertices.
315
+ EXPECT_EQ(orig.verts, 288u);
316
+ }
317
+
318
+ TEST(SphereStyle, AlignedPutsVerticesAtThePoles) {
319
+ Evaluated e = evalSrc("sphere(r=10, style=\"aligned\", $fn=24);");
320
+ ASSERT_EQ(e.bodies.size(), 1u);
321
+ const manifold::Box bb = e.bodies[0].body->BoundingBox();
322
+ // A pole vertex reaches the full radius; "orig" stops short of it.
323
+ EXPECT_NEAR(bb.max.z, 10.0, 1e-9);
324
+ EXPECT_NEAR(bb.min.z, -10.0, 1e-9);
325
+ const manifold::Box origBb = evalSrc("sphere(r=10, style=\"orig\", $fn=24);").bodies[0].body->BoundingBox();
326
+ EXPECT_LT(origBb.max.z, 10.0);
327
+ }
328
+
329
+ TEST(SphereStyle, AlignedTouchesTheAxesWhenFnIsDivisibleByFour) {
330
+ // $fn divisible by 4 puts a ring on the equator and a vertex on each of
331
+ // +-X and +-Y, so the bounding box reaches the full radius on every
332
+ // axis. That is the whole point of the style.
333
+ Evaluated e = evalSrc("sphere(r=10, style=\"aligned\", $fn=24);");
334
+ const manifold::Box bb = e.bodies[0].body->BoundingBox();
335
+ EXPECT_NEAR(bb.max.x, 10.0, 1e-9);
336
+ EXPECT_NEAR(bb.max.y, 10.0, 1e-9);
337
+ // ...whereas "orig" has no vertex on either axis and falls inside.
338
+ const manifold::Box ob = evalSrc("sphere(r=10, style=\"orig\", $fn=24);").bodies[0].body->BoundingBox();
339
+ EXPECT_LT(ob.max.x, 10.0);
340
+ }
341
+
342
+ TEST(SphereStyle, StaggerHasTheSameCountsAsAlignedButDiffers) {
343
+ const SphereStats a = sphereStats("sphere(r=10, style=\"aligned\", $fn=24);");
344
+ const SphereStats s = sphereStats("sphere(r=10, style=\"stagger\", $fn=24);");
345
+ EXPECT_EQ(a.verts, s.verts); // same lattice size...
346
+ EXPECT_EQ(a.tris, s.tris);
347
+ EXPECT_GT(std::fabs(a.volume - s.volume), 1.0); // ...different shape
348
+ }
349
+
350
+ TEST(SphereStyle, EveryStyleMatchesBosl2sCounts) {
351
+ // Compared against BOSL2 spheroid(r=10, style=..., circum=false, $fn=24).
352
+ EXPECT_EQ(sphereStats("sphere(r=10, style=\"aligned\", $fn=24);").verts, 266u);
353
+ EXPECT_EQ(sphereStats("sphere(r=10, style=\"aligned\", $fn=24);").tris, 528u);
354
+ EXPECT_EQ(sphereStats("sphere(r=10, style=\"stagger\", $fn=24);").verts, 266u);
355
+ EXPECT_EQ(sphereStats("sphere(r=10, style=\"octa\", $fn=24);").verts, 146u);
356
+ EXPECT_EQ(sphereStats("sphere(r=10, style=\"octa\", $fn=24);").tris, 288u);
357
+ EXPECT_EQ(sphereStats("sphere(r=10, style=\"icosa\", $fn=24);").verts, 252u);
358
+ EXPECT_EQ(sphereStats("sphere(r=10, style=\"icosa\", $fn=24);").tris, 500u);
359
+ }
360
+
361
+ TEST(SphereStyle, EveryStyleMatchesBosl2sVolume) {
362
+ // aligned/stagger/icosa agree with BOSL2 to the last digit. "octa" comes
363
+ // from Manifold::Sphere, whose octahedral subdivision distributes its
364
+ // vertices a little differently than BOSL2's -- same 146/288 topology,
365
+ // ~0.05% apart in volume.
366
+ EXPECT_NEAR(sphereStats("sphere(r=10, style=\"aligned\", $fn=24);").volume, 4070.5524, 1e-3);
367
+ EXPECT_NEAR(sphereStats("sphere(r=10, style=\"stagger\", $fn=24);").volume, 4082.1246, 1e-3);
368
+ EXPECT_NEAR(sphereStats("sphere(r=10, style=\"icosa\", $fn=24);").volume, 4097.2467, 1e-3);
369
+ EXPECT_NEAR(sphereStats("sphere(r=10, style=\"octa\", $fn=24);").volume, 4024.3239, 1e-3);
370
+ }
371
+
372
+ TEST(SphereStyle, EveryStyleIsAClosedSolidUnderTheTrueVolume) {
373
+ for (const char* st : {"orig", "aligned", "stagger", "octa", "icosa"}) {
374
+ const SphereStats s = sphereStats(std::string("sphere(r=10, style=\"") + st + "\", $fn=24);");
375
+ // Positive volume also means the winding is outward: an inverted
376
+ // solid reports a NEGATIVE volume here, which is how the first cut
377
+ // of aligned/stagger was caught. Checking |volume| would have missed
378
+ // it entirely.
379
+ EXPECT_GT(s.volume, 3900.0) << st;
380
+ EXPECT_LT(s.volume, 4188.7902) << st; // strictly inside the true sphere
381
+ EXPECT_GT(s.tris, 100u) << st;
382
+ }
383
+ }
384
+
385
+ TEST(SphereStyle, UnknownStyleWarnsAndFallsBackToOrig) {
386
+ std::string last;
387
+ Evaluated e = evalSrc("sphere(r=10, style=\"banana\", $fn=24);",
388
+ [&](const std::string& m) { last = m; });
389
+ EXPECT_NE(last.find("unknown style"), std::string::npos) << last;
390
+ ASSERT_EQ(e.bodies.size(), 1u);
391
+ EXPECT_EQ(static_cast<size_t>(e.bodies[0].body->NumVert()), 288u); // orig
392
+ }
393
+
394
+ TEST(SphereStyle, StyleRespectsFnAndRadius) {
395
+ // Denser $fn subdivides further, and the radius scales the result.
396
+ EXPECT_GT(sphereStats("sphere(r=10, style=\"icosa\", $fn=64);").tris,
397
+ sphereStats("sphere(r=10, style=\"icosa\", $fn=24);").tris);
398
+ const double v10 = sphereStats("sphere(r=10, style=\"aligned\", $fn=24);").volume;
399
+ const double v20 = sphereStats("sphere(r=20, style=\"aligned\", $fn=24);").volume;
400
+ EXPECT_NEAR(v20 / v10, 8.0, 1e-6); // volume goes as r^3
401
+ }
@@ -55,7 +55,7 @@ wheels = [
55
55
 
56
56
  [[package]]
57
57
  name = "openscad-cpp-evaluator"
58
- version = "0.35.0"
58
+ version = "0.36.0"
59
59
  source = { editable = "." }
60
60
  dependencies = [
61
61
  { name = "numpy" },