openscad-cpp-evaluator 1.27.0__tar.gz → 1.28.1__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 (221) hide show
  1. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/CLAUDE.md +13 -7
  2. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +6 -1
  4. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/pyproject.toml +1 -1
  5. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/dxf_dim.cpp +29 -6
  6. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/import.cpp +37 -7
  7. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/primitives_3d.cpp +15 -8
  8. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/coverage.cpp +15 -0
  9. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/export.cpp +5 -1
  10. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/import/svg_import.cpp +103 -2
  11. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_coverage.cpp +36 -0
  12. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_dxf_svg_import.cpp +98 -1
  13. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_export.cpp +47 -0
  14. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_import_export.cpp +33 -2
  15. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_primitives.cpp +29 -0
  16. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/uv.lock +1 -1
  17. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/.github/workflows/ci.yml +0 -0
  18. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/.github/workflows/wheels.yml +0 -0
  19. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/.gitignore +0 -0
  20. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/.gitmodules +0 -0
  21. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/CMakeLists.txt +0 -0
  22. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/LICENSE +0 -0
  23. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/README.md +0 -0
  24. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/bindings/CMakeLists.txt +0 -0
  25. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/bindings/ast_to_py.cpp +0 -0
  26. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/bindings/module.cpp +0 -0
  27. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/cmake/embed_font.cmake +0 -0
  28. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/examples/CMakeLists.txt +0 -0
  29. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/examples/eval_perf_benchmark.cpp +0 -0
  30. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/examples/manifold_cache_reuse.cpp +0 -0
  31. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/examples/minimal_debugger.cpp +0 -0
  32. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  33. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/.gitignore +0 -0
  34. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  35. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  36. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/README.md +0 -0
  37. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  38. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  39. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  40. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  41. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  42. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  43. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  44. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  45. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  46. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  47. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  48. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  49. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  50. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  51. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  52. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  53. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  54. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/api.cpp +0 -0
  55. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  56. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  57. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  58. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  59. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  60. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  61. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  62. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  63. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  64. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  65. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  66. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  67. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  68. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  69. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  70. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  71. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/position.cpp +0 -0
  72. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  73. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  74. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  75. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  76. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  77. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  78. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  79. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  80. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  81. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  82. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  83. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  84. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  85. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  86. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  87. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  88. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  89. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  90. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  91. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  92. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  93. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  94. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  95. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  96. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  97. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  98. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  99. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  100. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  101. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  102. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  103. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  104. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  105. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  106. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  107. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/coverage.hpp +0 -0
  108. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  109. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  110. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  111. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  112. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  113. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  114. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  115. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  116. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  117. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/export.hpp +0 -0
  118. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  119. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  120. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  121. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  122. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  123. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  124. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  125. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  126. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  127. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  128. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  129. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  130. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  131. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  132. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  133. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  134. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/value.hpp +0 -0
  135. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  136. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/python/openscad_cpp_evaluator/__init__.py +0 -0
  137. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/resources/fonts/LICENSE +0 -0
  138. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  139. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  140. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  141. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  142. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/CMakeLists.txt +0 -0
  143. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/booleans.cpp +0 -0
  144. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/builtins.hpp +0 -0
  145. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/call_args.cpp +0 -0
  146. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/color.cpp +0 -0
  147. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/control.cpp +0 -0
  148. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/extrude.cpp +0 -0
  149. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/function_builtins.cpp +0 -0
  150. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/modifiers.cpp +0 -0
  151. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/primitives_2d.cpp +0 -0
  152. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/registry.cpp +0 -0
  153. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/roof.cpp +0 -0
  154. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/segments.cpp +0 -0
  155. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/surface.cpp +0 -0
  156. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/text.cpp +0 -0
  157. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/topology.cpp +0 -0
  158. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/builtins/transforms.cpp +0 -0
  159. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/bytecode_compiler.cpp +0 -0
  160. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/bytecode_vm.cpp +0 -0
  161. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/colored_body.cpp +0 -0
  162. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/csg_generate.cpp +0 -0
  163. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/csg_resolve.cpp +0 -0
  164. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/css_colors.cpp +0 -0
  165. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/debug/debug_repl.cpp +0 -0
  166. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/debug_profile.cpp +0 -0
  167. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/eval_context.cpp +0 -0
  168. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/eval_error.cpp +0 -0
  169. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/eval_use.cpp +0 -0
  170. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/expr_eval.cpp +0 -0
  171. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/format_closure.cpp +0 -0
  172. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/import/dxf_import.cpp +0 -0
  173. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/import/mesh_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/import/surface_load.cpp +0 -0
  175. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/manifold_cache.cpp +0 -0
  176. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/measure_geometry.cpp +0 -0
  177. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/mesh_check.cpp +0 -0
  178. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/stmt_eval.cpp +0 -0
  179. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/text/font_match.cpp +0 -0
  180. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/text/freetype_font_provider.cpp +0 -0
  181. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/text/text_metrics.cpp +0 -0
  182. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/user_calls.cpp +0 -0
  183. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/utf8.cpp +0 -0
  184. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/value.cpp +0 -0
  185. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/src/zip_stored.cpp +0 -0
  186. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/CMakeLists.txt +0 -0
  187. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_booleans.cpp +0 -0
  188. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_bytecode_compiler.cpp +0 -0
  189. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_children_separate.cpp +0 -0
  190. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_cli.cpp +0 -0
  191. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_control_flow.cpp +0 -0
  192. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_csg_tree.cpp +0 -0
  193. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_debug_hooks.cpp +0 -0
  194. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_eval_context.cpp +0 -0
  195. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_expr_eval.cpp +0 -0
  196. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_extrude_roof.cpp +0 -0
  197. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_feature_detection.cpp +0 -0
  198. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_function_builtins.cpp +0 -0
  199. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_helpers.hpp +0 -0
  200. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_include_cache.cpp +0 -0
  201. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_manifold_cache.cpp +0 -0
  202. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_mesh_check.cpp +0 -0
  203. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_multi_color_merge.cpp +0 -0
  204. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_profiling.cpp +0 -0
  205. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_python_bindings.py +0 -0
  206. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_render_expr.cpp +0 -0
  207. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_scope_trail.cpp +0 -0
  208. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_scoping.cpp +0 -0
  209. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_surface.cpp +0 -0
  210. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_tail_calls.cpp +0 -0
  211. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_text.cpp +0 -0
  212. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_transforms.cpp +0 -0
  213. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_unexpected_args.cpp +0 -0
  214. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_use.cpp +0 -0
  215. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_value.cpp +0 -0
  216. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_viewport_params.cpp +0 -0
  217. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tests/test_zip_stored.cpp +0 -0
  218. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tools/cli/CMakeLists.txt +0 -0
  219. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tools/cli/cli_lib.cpp +0 -0
  220. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tools/cli/cli_lib.hpp +0 -0
  221. {openscad_cpp_evaluator-1.27.0 → openscad_cpp_evaluator-1.28.1}/tools/cli/main.cpp +0 -0
@@ -984,15 +984,16 @@ grep for `ponytail:`.
984
984
  rather than out of the model. Ported from the reference's `io/dxfdim.cc` plus the DIMENSION/LINE
985
985
  half of `io/DxfData.cc`: `dxf_dim()` returns the first matching DIMENSION's value by its type
986
986
  (`70 & 7` — 0 rotated, 1 aligned, 2 angular, 3/4 diameter/radius, 6 ordinate; type 5 is
987
- unsupported there too), `dxf_cross()` intersects the first two LINE entities. Note the reference's
987
+ unsupported there too), `dxf_cross()` intersects the first two 2-point paths. Note the reference's
988
988
  own coordinate quirk, reproduced here: groups 11/12/16 (and 21/22/26) are scaled but NOT
989
989
  origin-shifted, being extents rather than positions.
990
990
 
991
991
  Deliberately its own group-code walk rather than sharing `src/import/dxf_import.cpp`'s: that one
992
992
  produces closed contours, and these need the DIMENSION entities' seven coordinate slots and the
993
- raw LINE endpoints, neither of which a contour list keeps. One divergence: `dxf_cross()` takes the
994
- first two LINEs, where the reference walks every 2-point path — for a real cross (two strokes
995
- meeting in the middle, not at their endpoints) that is the same set.
993
+ raw LINE endpoints, neither of which a contour list keeps. A 2-point path is a LINE sharing
994
+ neither end with another LINE or an ARC (an ARC's ends read from its centre, radius and 50/51
995
+ angles): one that does belongs to an outline. Taking the first two LINEs outright found a "cross"
996
+ in example009.dxf's `fan_top` outline where the reference warns `Can't find cross`.
996
997
 
997
998
  - `src/csg_generate.cpp` — `Evaluator::applyDimensionRules`, run centrally just before every node's
998
999
  `GenerateFn` (and once more over the top-level list, which is an implicit union). Mirrors the
@@ -1210,9 +1211,14 @@ grep for `ponytail:`.
1210
1211
  path-command tokenizer/flattener (`M`/`L`/`H`/`V`/`C`/`S`/`Q`/`T`/`A`, both absolute and relative,
1211
1212
  cubic/quadratic Bezier flattening, full elliptical-arc endpoint-to-center parameterization) plus
1212
1213
  `rect`/`circle`/`ellipse`/`polygon`/`polyline` and nested `transform="matrix()/translate()/
1213
- scale()/rotate()"` composition (including through `<g>` groups) — Y is flipped (SVG down →
1214
- OpenSCAD up) at the point-transform step, `applyMat()` (named to avoid colliding with
1215
- `std::apply` in an ADL lookup that broke the build once — see its own comment).
1214
+ scale()/rotate()"` composition (including through `<g>` groups), in SVG user units at the
1215
+ point-transform step, `applyMat()` (named to avoid colliding with `std::apply` in an ADL lookup
1216
+ that broke the build once — see its own comment). **Placement is `pageMap()`, a port of the
1217
+ reference's `import_svg.cc`**: the page's `width`/`height` to mm (a unitless length at `dpi=`,
1218
+ 72 by default; `px` at 96), the `viewBox` scaled onto the page under `preserveAspectRatio`
1219
+ (default `xMidYMid meet`), and Y flipped about the page HEIGHT -- or about the drawing's centre
1220
+ with `center=true`. Negating Y alone, as this did before, put a unitless 100-unit drawing in 2.8x
1221
+ too large and below the X axis. Bounds match OpenSCAD 2026.02.01 (`SvgPlacement` tests).
1216
1222
  - `include/openscad_cpp_evaluator/font_provider.hpp` — the `FontProvider` abstract interface
1217
1223
  (`resolveFont`/`metrics`/`shapeText`/`glyphInkBounds`/`glyphOutline`), the seam that keeps
1218
1224
  `text()`/`textmetrics()`/`fontmetrics()` decoupled from any specific font backend. `Evaluator`'s
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.27.0
3
+ Version: 1.28.1
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -34,8 +34,13 @@ struct SvgFilter {
34
34
  // `matched`, when given, reports whether the filter found anything. A miss
35
35
  // imports nothing rather than falling back to the whole drawing; the caller
36
36
  // warns. Always true when no filter was set.
37
+ // Placed as OpenSCAD places them: page units to mm (a unitless length at
38
+ // `dpi`), the viewBox under preserveAspectRatio, Y flipped about the page
39
+ // height -- or about the drawing's centre with `center`.
37
40
  std::vector<Contour2d> loadSvgContours(const std::string& path,
38
41
  const SvgFilter& filter = {},
39
- bool* matched = nullptr);
42
+ bool* matched = nullptr,
43
+ double dpi = 72.0,
44
+ bool center = false);
40
45
 
41
46
  } // namespace oscadeval
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.27.0"
7
+ version = "1.28.1"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -6,6 +6,7 @@
6
6
  #include <cmath>
7
7
  #include <filesystem>
8
8
  #include <fstream>
9
+ #include <map>
9
10
  #include <sstream>
10
11
 
11
12
  namespace oscadeval {
@@ -26,7 +27,9 @@ struct Entity {
26
27
  std::string layer;
27
28
  std::string name; // group 1: DIMENSION's text override
28
29
  int dimType = 0; // group 70
29
- double angle = 0.0; // group 50
30
+ double angle = 0.0; // group 50 (an ARC's start angle)
31
+ double angle2 = 0.0; // group 51: an ARC's end angle
32
+ double radius = 0.0; // group 40: an ARC's radius
30
33
  double coords[7][2] = {}; // groups 10-16 / 20-26
31
34
  std::vector<double> xverts, yverts; // groups 10/11 and 20/21, in order
32
35
  };
@@ -91,6 +94,8 @@ std::vector<Entity> readEntities(const std::string& path, double xorigin, double
91
94
  case 20:
92
95
  case 21: cur.yverts.push_back((num() - yorigin) * scale); break;
93
96
  case 50: cur.angle = num(); break;
97
+ case 51: cur.angle2 = num(); break;
98
+ case 40: cur.radius = num() * scale; break;
94
99
  case 70:
95
100
  try {
96
101
  cur.dimType = std::stoi(data);
@@ -188,16 +193,34 @@ Value builtinDxfCross(Evaluator& ev, const CallArgs& args, const oscad::ASTNode&
188
193
  return Value{};
189
194
  }
190
195
 
191
- // The first two LINEs on the layer are taken to be the cross. The
192
- // reference walks every 2-point path instead, which for a real cross is
193
- // the same set: its two strokes meet in the middle rather than at their
194
- // endpoints, so neither gets joined into a longer path.
196
+ // The first two 2-POINT PATHS on the layer are the cross, as the
197
+ // reference walks them: a LINE sharing an end with another LINE or an
198
+ // ARC is joined into a longer path, part of an outline, and no stroke of
199
+ // a cross. Taking the first two LINEs outright found a "cross" in any
200
+ // outline -- example009.dxf's fan_top, where the reference finds none.
201
+ // ponytail: LINE and ARC ends only; polylines and splines don't
202
+ // disqualify a LINE here.
203
+ const std::vector<Entity> entities = readEntities(c.path, c.xorigin, c.yorigin, c.scale);
204
+ std::map<std::pair<long long, long long>, int> ends;
205
+ const auto endKey = [](double x, double y) { return std::make_pair(std::llround(x * 1e6), std::llround(y * 1e6)); };
206
+ for (const Entity& e : entities) {
207
+ if (!c.layer.empty() && c.layer != e.layer) continue;
208
+ if (e.type == "LINE" && e.xverts.size() >= 2 && e.yverts.size() >= 2) {
209
+ ++ends[endKey(e.xverts[0], e.yverts[0])];
210
+ ++ends[endKey(e.xverts[1], e.yverts[1])];
211
+ } else if (e.type == "ARC") {
212
+ for (double a : {e.angle, e.angle2}) {
213
+ ++ends[endKey(e.coords[0][0] + e.radius * std::cos(rad(a)), e.coords[0][1] + e.radius * std::sin(rad(a)))];
214
+ }
215
+ }
216
+ }
195
217
  double p[4][2];
196
218
  int found = 0;
197
- for (const Entity& e : readEntities(c.path, c.xorigin, c.yorigin, c.scale)) {
219
+ for (const Entity& e : entities) {
198
220
  if (e.type != "LINE") continue;
199
221
  if (!c.layer.empty() && c.layer != e.layer) continue;
200
222
  if (e.xverts.size() < 2 || e.yverts.size() < 2) continue;
223
+ if (ends[endKey(e.xverts[0], e.yverts[0])] > 1 || ends[endKey(e.xverts[1], e.yverts[1])] > 1) continue;
201
224
  p[found][0] = e.xverts[0];
202
225
  p[found][1] = e.yverts[0];
203
226
  ++found;
@@ -199,8 +199,18 @@ CSGParams resolveImport(Evaluator& ev, const oscad::ModularCall& node, EvalConte
199
199
  const std::string path = resolveFilePath(fileArg, node);
200
200
  const std::string ext = lowerExt(path);
201
201
 
202
+ if (!std::filesystem::exists(path)) {
203
+ // A missing file warns and imports nothing, as in OpenSCAD; it
204
+ // aborted the whole render. Warned at generate, where OpenSCAD does,
205
+ // so it follows the script's echoes.
206
+ params["kind"] = Value{std::string("missing")};
207
+ params["path"] = Value{path};
208
+ params["dxf"] = Value{ext == ".dxf"};
209
+ return params;
210
+ }
202
211
  if (ext == ".dxf" || ext == ".svg" || ext == ".pdf") {
203
212
  std::vector<Contour2d> contours;
213
+ bool filteredMiss = false;
204
214
  try {
205
215
  if (ext == ".dxf") {
206
216
  std::optional<std::string> layer;
@@ -211,7 +221,10 @@ CSGParams resolveImport(Evaluator& ev, const oscad::ModularCall& node, EvalConte
211
221
  if (const std::string* v = std::get_if<std::string>(&idArg)) filter.id = *v;
212
222
  if (const std::string* v = std::get_if<std::string>(&classArg)) filter.cls = *v;
213
223
  bool matched = true;
214
- contours = loadSvgContours(path, filter, &matched);
224
+ const Value dpiArg = getArg(args, std::nullopt, "dpi", Value{72.0});
225
+ contours = loadSvgContours(path, filter, &matched,
226
+ std::holds_alternative<double>(dpiArg) ? std::get<double>(dpiArg) : 72.0,
227
+ truthy(getArg(args, std::nullopt, "center", Value{false})));
215
228
  if (!matched) {
216
229
  // Nothing imported, rather than silently falling back to
217
230
  // the whole drawing -- a cut layer that quietly became
@@ -222,14 +235,15 @@ CSGParams resolveImport(Evaluator& ev, const oscad::ModularCall& node, EvalConte
222
235
  if (!what.empty()) what += ", ";
223
236
  what += "class = \"" + *filter.cls + "\"";
224
237
  }
225
- ev.warn("import() filter " + what + " did not match anything in "
226
- + path, &node.position());
238
+ // OpenSCAD's words, which name no file.
239
+ ev.warn("import() filter " + what + " did not match anything", &node.position());
240
+ filteredMiss = true;
227
241
  }
228
242
  }
229
243
  } catch (const std::exception& e) {
230
244
  ev.error(std::string("import: ") + e.what(), node);
231
245
  }
232
- if (contours.empty()) {
246
+ if (contours.empty() && !filteredMiss) {
233
247
  ev.error(ext == ".dxf" ? "import: no closed contours found in DXF file" : "import: no shapes found in SVG file", node);
234
248
  }
235
249
  params["kind"] = Value{std::string("region")};
@@ -276,6 +290,16 @@ std::vector<ColoredBody> generateImport(Evaluator& ev, const CSGParams& params,
276
290
  const auto kindIt = params.find("kind");
277
291
  if (kindIt == params.end()) return {};
278
292
  const std::string& kind = std::get<std::string>(kindIt->second);
293
+ if (kind == "missing") {
294
+ // OpenSCAD's own wording, which names a line rather than a file.
295
+ const std::string& path = std::get<std::string>(params.at("path"));
296
+ ev.warn(std::get<bool>(params.at("dxf"))
297
+ ? "Can't open DXF file '" + path + "'."
298
+ : "Can't open import file '" + path + "', import() at line " +
299
+ std::to_string(node.position().line),
300
+ nullptr);
301
+ return {};
302
+ }
279
303
  if (kind == "region") {
280
304
  const std::vector<Contour2d> contours = valueToContours(params.at("contours"));
281
305
  manifold::Polygons polys;
@@ -397,6 +421,10 @@ Value importAsValue(Evaluator& ev, const CallArgs& args, const oscad::ASTNode& n
397
421
  }
398
422
  const std::string path = resolveFilePath(fileArg, node);
399
423
  const std::string ext = lowerExt(path);
424
+ if (!std::filesystem::exists(path)) {
425
+ ev.warn("Could not read file '" + path + "'", &node.position());
426
+ return Value{};
427
+ }
400
428
 
401
429
  try {
402
430
  if (ext == ".json") return loadJsonAsValue(path);
@@ -412,7 +440,10 @@ Value importAsValue(Evaluator& ev, const CallArgs& args, const oscad::ASTNode& n
412
440
  if (const std::string* v = std::get_if<std::string>(&idArg)) filter.id = *v;
413
441
  if (const std::string* v = std::get_if<std::string>(&classArg)) filter.cls = *v;
414
442
  bool matched = true;
415
- contours = loadSvgContours(path, filter, &matched);
443
+ const Value dpiArg = getArg(args, std::nullopt, "dpi", Value{72.0});
444
+ contours = loadSvgContours(path, filter, &matched,
445
+ std::holds_alternative<double>(dpiArg) ? std::get<double>(dpiArg) : 72.0,
446
+ truthy(getArg(args, std::nullopt, "center", Value{false})));
416
447
  if (!matched) {
417
448
  // Nothing imported, rather than silently falling back to
418
449
  // the whole drawing -- a cut layer that quietly became
@@ -423,8 +454,7 @@ Value importAsValue(Evaluator& ev, const CallArgs& args, const oscad::ASTNode& n
423
454
  if (!what.empty()) what += ", ";
424
455
  what += "class = \"" + *filter.cls + "\"";
425
456
  }
426
- ev.warn("import() filter " + what + " did not match anything in "
427
- + path, &node.position());
457
+ ev.warn("import() filter " + what + " did not match anything", &node.position());
428
458
  }
429
459
  }
430
460
  return contoursToValue(contours);
@@ -102,18 +102,25 @@ std::string describeEdge(const M& mesh, std::pair<uint32_t, uint32_t> edge) {
102
102
 
103
103
  CSGParams resolveCube(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
104
104
  auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
105
- Value sizeArg = getArg(args, 0, "size", Value{1.0});
105
+ const Value sizeArg = getArg(args, 0, "size", Value{});
106
106
  const bool center = truthy(getArg(args, 1, "center", Value{false}));
107
107
 
108
- std::vector<Value> sizeVec;
108
+ // As OpenSCAD reads it: undef (absent or explicit) is the default of 1, a
109
+ // number or three numbers are the size, and anything else warns and
110
+ // falls back to 1. Explicit undef built a zero-size cube -- nothing --
111
+ // and a 2-vector or a string was converted leniently, without a word.
112
+ std::vector<Value> sizeVec(3, Value{1.0});
113
+ const ListPtr* l = std::get_if<ListPtr>(&sizeArg);
109
114
  if (const double* s = std::get_if<double>(&sizeArg)) {
110
115
  sizeVec = {Value{*s}, Value{*s}, Value{*s}};
111
- } else if (const ListPtr* l = std::get_if<ListPtr>(&sizeArg); l && *l) {
112
- for (size_t i = 0; i < 3; ++i) {
113
- sizeVec.push_back(Value{i < (*l)->items.size() ? toDoubleLenient((*l)->items[i]) : 0.0});
114
- }
115
- } else {
116
- sizeVec = {Value{0.0}, Value{0.0}, Value{0.0}};
116
+ } else if (l && *l && (*l)->items.size() == 3 &&
117
+ std::all_of((*l)->items.begin(), (*l)->items.end(),
118
+ [](const Value& v) { return std::holds_alternative<double>(v); })) {
119
+ sizeVec = (*l)->items;
120
+ } else if (!std::holds_alternative<std::monostate>(sizeArg)) {
121
+ ev.warn("Unable to convert cube(size=" + fmtValue(sizeArg) +
122
+ ", ...) parameter to a number or a vec3 of numbers",
123
+ &node.position());
117
124
  }
118
125
 
119
126
  CSGParams params;
@@ -7,6 +7,7 @@
7
7
 
8
8
  #include <algorithm>
9
9
  #include <map>
10
+ #include <tuple>
10
11
  #include <unordered_set>
11
12
 
12
13
  namespace oscadeval {
@@ -354,8 +355,22 @@ CoverageResult buildCoverageResult(const std::vector<const ASTNode*>& roots,
354
355
  const std::vector<const ASTNode*>& extraStatements,
355
356
  const CoverageRecorder& recorder) {
356
357
  CoverageResult result;
358
+ // One span per source position. A file several files use<> is parsed
359
+ // once per use, so its nodes arrive once per parse -- BOSL2's
360
+ // builtins.scad three times, two of them with zero hits -- which
361
+ // counted its spans three times over and pulled its percentages down.
362
+ // Merged here, hits summed: each copy's executions are real ones.
363
+ std::map<std::tuple<std::string, int, int, CoverageKind>, size_t> index;
357
364
  for (const Coverable& c : collectCoverable(roots, extraStatements)) {
358
365
  const oscad::Position& p = c.node->position();
366
+ const auto key = std::make_tuple(p.origin, p.start_offset, p.end_offset, c.kind);
367
+ const auto [it, fresh] = index.emplace(key, result.spans.size());
368
+ if (!fresh) {
369
+ CoverageSpan& s = result.spans[it->second];
370
+ s.hits += recorder.hitsFor(*c.node);
371
+ s.arm = s.arm || c.arm;
372
+ continue;
373
+ }
359
374
  result.spans.push_back(CoverageSpan{p.origin, p.line, p.column, p.start_offset, p.end_offset, c.kind, c.arm,
360
375
  recorder.hitsFor(*c.node)});
361
376
  }
@@ -663,7 +663,11 @@ void writePly(const std::string& path, const std::vector<ExportObject>& objects)
663
663
  colors.push_back(rgb(o.triColors[t], 0));
664
664
  colors.push_back(rgb(o.triColors[t], 1));
665
665
  colors.push_back(rgb(o.triColors[t], 2));
666
- faces.push_back(static_cast<int32_t>(faces.size()) + base);
666
+ // The vertex just pushed. Not faces.size() + base: faces
667
+ // already holds every earlier object's entries, so that
668
+ // counted `base` twice and pointed past the vertex list
669
+ // on any object after the first.
670
+ faces.push_back(static_cast<int32_t>(verts.size() / 3) - 1);
667
671
  }
668
672
  }
669
673
  }
@@ -5,6 +5,7 @@
5
5
  #include <cmath>
6
6
  #include <cstring>
7
7
  #include <fstream>
8
+ #include <optional>
8
9
  #include <sstream>
9
10
  #include <stdexcept>
10
11
 
@@ -194,7 +195,7 @@ Mat3 compose(const Mat3& m, const Mat3& acc) {
194
195
  std::array<double, 2> applyMat(const std::array<double, 2>& pt, const Mat3& m) {
195
196
  const double x = m.a * pt[0] + m.c * pt[1] + m.e;
196
197
  const double y = m.b * pt[0] + m.d * pt[1] + m.f;
197
- return {x, -y}; // flip Y: SVG down -> OpenSCAD up
198
+ return {x, y}; // SVG user units; pageMap places them on the page
198
199
  }
199
200
 
200
201
  std::vector<double> parseNumberList(const std::string& s) {
@@ -489,11 +490,109 @@ void walkFiltered(const XmlNode& el, const Mat3& mat, const SvgFilter& f,
489
490
  for (const XmlNode& child : el.children) walkFiltered(child, m, f, out, matched);
490
491
  }
491
492
 
493
+ // "12", "12.5mm", "3in" -> (number, unit); unit empty when unitless.
494
+ // nullopt for anything else, which OpenSCAD treats as absent.
495
+ std::optional<std::pair<double, std::string>> parseLength(const std::string& text) {
496
+ const size_t b = text.find_first_not_of(" \t\r\n");
497
+ if (b == std::string::npos) return std::nullopt;
498
+ const size_t e = text.find_last_not_of(" \t\r\n");
499
+ const std::string t = text.substr(b, e - b + 1);
500
+ size_t used = 0;
501
+ double n = 0.0;
502
+ try {
503
+ n = std::stod(t, &used);
504
+ } catch (...) {
505
+ return std::nullopt;
506
+ }
507
+ std::string unit = t.substr(used);
508
+ unit.erase(0, unit.find_first_not_of(" \t"));
509
+ static const char* known[] = {"", "em", "ex", "px", "in", "cm", "mm", "pt", "pc", "%"};
510
+ for (const char* k : known) {
511
+ if (unit == k) return std::make_pair(n, unit);
512
+ }
513
+ return std::nullopt;
514
+ }
515
+
516
+ // Place SVG user-unit contours as OpenSCAD's import_svg.cc does: the page's
517
+ // width/height to millimetres (a unitless length at `dpi`, px at 96), the
518
+ // viewBox scaled onto it under preserveAspectRatio (default xMidYMid meet),
519
+ // and Y flipped about the page height -- or, with center, about the
520
+ // drawing's own centre. Without this a unitless 100-unit drawing came in
521
+ // 2.8x too large and below the X axis.
522
+ void pageMap(const XmlNode& root, double dpi, bool center, std::vector<Contour2d>& contours) {
523
+ std::vector<double> vb;
524
+ {
525
+ std::string v = root.getAttr("viewBox");
526
+ std::replace(v.begin(), v.end(), ',', ' ');
527
+ std::istringstream in(v);
528
+ double x = 0.0;
529
+ while (in >> x) vb.push_back(x);
530
+ }
531
+ const bool valid = vb.size() == 4 && vb[2] >= 0.0 && vb[3] >= 0.0;
532
+ const auto toMm = [&](const char* attr, double viewbox) {
533
+ const auto len = parseLength(root.getAttr(attr));
534
+ if (!len) return valid ? 25.4 * viewbox / dpi : 0.0; // absent: rely on dpi, as old Illustrator files do
535
+ const auto& [n, u] = *len;
536
+ if (u.empty()) return 25.4 * n / dpi;
537
+ if (u == "px") return 25.4 * n / 96.0;
538
+ if (u == "pt") return 25.4 * n / 72.0;
539
+ if (u == "pc") return 25.4 * n / 6.0;
540
+ if (u == "in") return 25.4 * n;
541
+ if (u == "cm") return 10.0 * n;
542
+ if (u == "mm") return n;
543
+ if (u == "%") return valid ? 25.4 * n / 100.0 * viewbox / dpi : 0.0;
544
+ return valid ? viewbox : 0.0; // em, ex
545
+ };
546
+ const double widthMm = toMm("width", valid ? vb[2] : 0.0);
547
+ const double heightMm = toMm("height", valid ? vb[3] : 0.0);
548
+ double sx = 1.0, sy = 1.0, vbx = 0.0, vby = 0.0, ax = 0.0, ay = 0.0;
549
+ if (valid) {
550
+ const auto w = parseLength(root.getAttr("width")), h = parseLength(root.getAttr("height"));
551
+ vbx = vb[0] * (w && w->second == "%" ? w->first / 100.0 : 1.0);
552
+ vby = vb[1] * (h && h->second == "%" ? h->first / 100.0 : 1.0);
553
+ sx = vb[2] != 0.0 ? widthMm / vb[2] : 0.0;
554
+ sy = vb[3] != 0.0 ? heightMm / vb[3] : 0.0;
555
+ std::istringstream par(root.getAttr("preserveAspectRatio"));
556
+ std::string align, meet;
557
+ par >> align;
558
+ if (align == "defer") par >> align;
559
+ par >> meet;
560
+ if (align.empty()) align = "xMidYMid";
561
+ if (align != "none") {
562
+ const double scaling = meet == "slice" ? std::max(sx, sy) : std::min(sx, sy);
563
+ sx = sy = scaling;
564
+ const auto frac = [](const std::string& a) { return a == "Min" ? 0.0 : a == "Max" ? 1.0 : 0.5; };
565
+ const bool wellFormed = align.size() == 8 && align[0] == 'x' && align[4] == 'Y';
566
+ const double fx = wellFormed ? frac(align.substr(1, 3)) : 0.5;
567
+ const double fy = wellFormed ? frac(align.substr(5, 3)) : 0.5;
568
+ ax = fx * (widthMm - sx * vb[2]);
569
+ ay = fy * (heightMm - sy * vb[3]);
570
+ }
571
+ }
572
+ double cx = -ax, cy = heightMm - ay;
573
+ if (center) {
574
+ double lo[2] = {INFINITY, INFINITY}, hi[2] = {-INFINITY, -INFINITY};
575
+ for (const Contour2d& c : contours)
576
+ for (const auto& p : c) {
577
+ lo[0] = std::min(lo[0], sx * p[0]);
578
+ hi[0] = std::max(hi[0], sx * p[0]);
579
+ lo[1] = std::min(lo[1], sy * p[1]);
580
+ hi[1] = std::max(hi[1], sy * p[1]);
581
+ }
582
+ cx = std::isfinite(lo[0]) ? (lo[0] + hi[0]) / 2.0 : 0.0;
583
+ cy = std::isfinite(lo[1]) ? (lo[1] + hi[1]) / 2.0 : 0.0;
584
+ }
585
+ // -vby - y, not y - vby: OpenSCAD's own formula, kept for parity.
586
+ for (Contour2d& c : contours)
587
+ for (auto& p : c) p = {sx * (p[0] - vbx) - cx, sy * (-vby - p[1]) + cy};
588
+ }
589
+
492
590
  } // namespace
493
591
 
494
592
  std::vector<Contour2d> loadSvgContours(const std::string& path,
495
593
  const SvgFilter& filter,
496
- bool* matched) {
594
+ bool* matched, double dpi, bool center) {
595
+ if (!(dpi > 0.0)) dpi = 72.0;
497
596
  std::ifstream in(path);
498
597
  if (!in) throw std::runtime_error("could not open '" + path + "'");
499
598
  std::stringstream buf;
@@ -505,6 +604,7 @@ std::vector<Contour2d> loadSvgContours(const std::string& path,
505
604
  if (!filter.id && !filter.cls) {
506
605
  if (matched) *matched = true; // no filter, nothing to miss
507
606
  walk(*root, Mat3{}, out);
607
+ pageMap(*root, dpi, center, out);
508
608
  return out;
509
609
  }
510
610
  bool hit = false;
@@ -513,6 +613,7 @@ std::vector<Contour2d> loadSvgContours(const std::string& path,
513
613
  // back to the whole drawing, which would be the silent-wrong-geometry
514
614
  // answer. The caller turns this into the warning.
515
615
  if (matched) *matched = hit;
616
+ pageMap(*root, dpi, center, out);
516
617
  return out;
517
618
  }
518
619
 
@@ -263,4 +263,40 @@ TEST(Coverage, IncludedAndUsedFilesCarryTheirOwnOrigin) {
263
263
  fs::remove_all(dir);
264
264
  }
265
265
 
266
+ // A file two included files each use<> is parsed once per use; its spans
267
+ // must still be listed once, with the hits of every copy, or its
268
+ // percentages count the unexecuted copies as missed code.
269
+ TEST(Coverage, AFileUsedTwiceIsListedOnce) {
270
+ namespace fs = std::filesystem;
271
+ const fs::path dir = fs::temp_directory_path() / "oscad_cov_twice";
272
+ fs::create_directories(dir);
273
+ const fs::path lib = dir / "lib.scad", a = dir / "a.scad", b = dir / "b.scad", main = dir / "main.scad";
274
+ std::ofstream(lib) << "function hit(x) = x;\nfunction miss(x) = x;\n";
275
+ std::ofstream(a) << "use <lib.scad>\nfunction fa() = hit(1);\n";
276
+ std::ofstream(b) << "use <lib.scad>\nfunction fb() = hit(2);\n";
277
+ std::ofstream(main) << "include <a.scad>\ninclude <b.scad>\necho(fa(), fb());\n";
278
+ auto ast = oscad::getASTFromFile(main.string());
279
+ ResolvedUseScopes uses = resolveUseScopes(ast, main.string(), {});
280
+ Evaluator ev({}, nullptr, nullptr, {}, false, /*coverage=*/true);
281
+ ev.setUsedFileGlobals(uses.usedFileGlobals);
282
+ EvalContext ctx = EvalContext::makeRoot(uses.rootScope.get());
283
+ ev.evaluate(uses.processedNodes, ctx, {}, false);
284
+ ASSERT_TRUE(ev.coverageResult.has_value());
285
+ std::map<int, std::vector<std::uint32_t>> libBodies;
286
+ for (const CoverageSpan& s : ev.coverageResult->spans) {
287
+ if (s.kind == CoverageKind::Body && fs::path(s.origin).filename() == "lib.scad") libBodies[s.line].push_back(s.hits);
288
+ }
289
+ ASSERT_EQ(libBodies[1].size(), 1u) << "one span per position, not one per parse";
290
+ EXPECT_EQ(libBodies[1][0], 2u);
291
+ ASSERT_EQ(libBodies[2].size(), 1u);
292
+ EXPECT_EQ(libBodies[2][0], 0u);
293
+ for (const CoverageFileSummary& f : ev.coverageResult->files) {
294
+ if (fs::path(f.origin).filename() == "lib.scad") {
295
+ EXPECT_EQ(f.bodies, 2u);
296
+ EXPECT_NEAR(f.body_percent(), 50.0, 1e-9);
297
+ }
298
+ }
299
+ fs::remove_all(dir);
300
+ }
301
+
266
302
  } // namespace oscadeval
@@ -1,3 +1,4 @@
1
+ #include "openscad_cpp_evaluator/dxf_svg_import.hpp"
1
2
  #include "openscad_cpp_evaluator/evaluator.hpp"
2
3
 
3
4
  #include "test_helpers.hpp"
@@ -459,6 +460,14 @@ const char* kDimDxf =
459
460
  // two parallel LINEs, which have no crossing point
460
461
  "0\nLINE\n8\npar\n10\n0\n20\n0\n11\n10\n21\n0\n"
461
462
  "0\nLINE\n8\npar\n10\n0\n20\n5\n11\n10\n21\n5\n"
463
+ // an outline: two LINEs joined end to end cross mid-way, but form one path
464
+ "0\nLINE\n8\noutline\n10\n0\n20\n0\n11\n10\n21\n10\n"
465
+ "0\nLINE\n8\noutline\n10\n10\n20\n10\n11\n0\n21\n10\n"
466
+ "0\nLINE\n8\noutline\n10\n0\n20\n10\n11\n10\n21\n0\n"
467
+ // a LINE closed by an ARC (centre (0,0), r 10, 0..90 degrees): a path too
468
+ "0\nLINE\n8\narc\n10\n10\n20\n0\n11\n0\n21\n10\n"
469
+ "0\nARC\n8\narc\n10\n0\n20\n0\n40\n10\n50\n0\n51\n90\n"
470
+ "0\nLINE\n8\narc\n10\n0\n20\n0\n11\n10\n21\n10\n"
462
471
  "0\nENDSEC\n0\nEOF\n";
463
472
 
464
473
  double dimOf(const std::string& call, Evaluator& ev) { return std::get<double>(asExpr(call, ev)); }
@@ -535,6 +544,22 @@ TEST(DxfCross, FindsTheCrossingPoint) {
535
544
  std::filesystem::remove(path);
536
545
  }
537
546
 
547
+ // The reference counts only 2-point paths: a LINE sharing an end with
548
+ // another LINE or an ARC belongs to an outline and is no stroke of a cross,
549
+ // even where two such lines do intersect. Checked against OpenSCAD
550
+ // 2026.02.01 on example009.dxf's fan_top layer.
551
+ TEST(DxfCross, LinesJoinedIntoAPathAreNoCross) {
552
+ const auto path = tempPath("dxfcross_paths.dxf");
553
+ writeFile(path, kDimDxf);
554
+ for (const char* layer : {"outline", "arc"}) {
555
+ std::string last;
556
+ Evaluator ev([&](const std::string& m) { last = m; });
557
+ EXPECT_TRUE(std::holds_alternative<std::monostate>(asExpr("dxf_cross(file=\"" + path.generic_string() + "\", layer=\"" + layer + "\")", ev)))
558
+ << layer;
559
+ EXPECT_NE(last.find("Can't find cross"), std::string::npos) << layer << ": " << last;
560
+ }
561
+ }
562
+
538
563
  TEST(DxfCross, ParallelOrTooFewLinesWarns) {
539
564
  const auto path = tempPath("cross_par.dxf");
540
565
  writeFile(path, kDimDxf);
@@ -629,7 +654,8 @@ TEST(SvgFilter, AnEnclosingTransformStillApplies) {
629
654
  ASSERT_EQ(pts.size(), 4u);
630
655
  double minx = 1e9;
631
656
  for (const auto& p : pts) minx = std::min(minx, std::get<double>(std::get<ListPtr>(p)->items[0]));
632
- EXPECT_NEAR(minx, 10.0, 1e-9);
657
+ // x = 10 on a unitless 100-unit page, which OpenSCAD reads at 72 dpi.
658
+ EXPECT_NEAR(minx, 10.0 * 25.4 / 72.0, 1e-9);
633
659
  std::filesystem::remove(path);
634
660
  }
635
661
 
@@ -652,3 +678,74 @@ TEST(SvgFilter, IdAndClassTogetherMatchEither) {
652
678
  EXPECT_EQ(contourCount(path.generic_string(), ", id=\"other\", class=\"cut\"", ev), 2u);
653
679
  std::filesystem::remove(path);
654
680
  }
681
+
682
+ namespace oscadeval {
683
+ namespace {
684
+
685
+ // SVG placement as OpenSCAD 2026.02.01 does it: page units to mm, the
686
+ // viewBox under preserveAspectRatio, Y flipped about the page height (or
687
+ // about the drawing's centre with center=true). Bounds read off the
688
+ // reference's own export for each document, with and without center.
689
+ struct SvgPlacementCase {
690
+ const char* attrs;
691
+ const char* body;
692
+ bool center;
693
+ double bounds[4];
694
+ };
695
+
696
+ TEST(SvgPlacement, MatchesTheReference) {
697
+ const SvgPlacementCase cases[] = {
698
+ {R"(width="100" height="100" viewBox="0 0 100 100")",
699
+ R"(<rect x="0" y="0" width="10" height="10"/><rect x="20" y="50" width="30" height="5"/>)", false,
700
+ {0.0, 15.875, 17.639, 35.278}},
701
+ {R"(width="100" height="100" viewBox="0 0 100 100")",
702
+ R"(<rect x="0" y="0" width="10" height="10"/><rect x="20" y="50" width="30" height="5"/>)", true,
703
+ {-8.819, -9.701, 8.819, 9.701}},
704
+ {R"(width="50mm" height="20mm" viewBox="10 5 200 40")",
705
+ R"(<rect x="10" y="5" width="20" height="10"/><rect x="92" y="17" width="16" height="16"/>)", false,
706
+ {0.0, 5.5, 24.5, 12.5}},
707
+ {R"(width="4in" height="2in" viewBox="0 0 100 100" preserveAspectRatio="xMaxYMin meet")",
708
+ R"(<rect x="0" y="0" width="100" height="100"/>)", false, {50.8, 0.0, 101.6, 50.8}},
709
+ {R"(width="4in" height="2in" viewBox="0 0 100 100" preserveAspectRatio="xMaxYMin meet")",
710
+ R"(<rect x="0" y="0" width="100" height="100"/>)", true, {-25.4, -25.4, 25.4, 25.4}},
711
+ {R"(width="300px" height="150px")", R"(<rect x="10" y="10" width="30" height="20"/>)", false,
712
+ {10.0, 9.688, 40.0, 29.688}},
713
+ {R"(viewBox="0 0 200 100")", R"(<rect x="0" y="0" width="200" height="100"/>)", false,
714
+ {0.0, 0.0, 70.556, 35.278}},
715
+ {R"(viewBox="0 0 200 100")", R"(<rect x="0" y="0" width="200" height="100"/>)", true,
716
+ {-35.278, -17.639, 35.278, 17.639}},
717
+ };
718
+ int n = 0;
719
+ for (const SvgPlacementCase& c : cases) {
720
+ const auto path = std::filesystem::temp_directory_path() / ("svg_place_" + std::to_string(n++) + ".svg");
721
+ std::ofstream(path) << "<svg xmlns=\"http://www.w3.org/2000/svg\" " << c.attrs << ">" << c.body << "</svg>";
722
+ const std::vector<Contour2d> contours = loadSvgContours(path.string(), {}, nullptr, 72.0, c.center);
723
+ double lo[2] = {1e18, 1e18}, hi[2] = {-1e18, -1e18};
724
+ for (const Contour2d& ct : contours)
725
+ for (const auto& p : ct)
726
+ for (int a = 0; a < 2; ++a) {
727
+ lo[a] = std::min(lo[a], p[a]);
728
+ hi[a] = std::max(hi[a], p[a]);
729
+ }
730
+ EXPECT_NEAR(lo[0], c.bounds[0], 1e-3) << c.attrs << " center=" << c.center;
731
+ EXPECT_NEAR(lo[1], c.bounds[1], 1e-3) << c.attrs << " center=" << c.center;
732
+ EXPECT_NEAR(hi[0], c.bounds[2], 1e-3) << c.attrs << " center=" << c.center;
733
+ EXPECT_NEAR(hi[1], c.bounds[3], 1e-3) << c.attrs << " center=" << c.center;
734
+ std::filesystem::remove(path);
735
+ }
736
+ }
737
+
738
+ TEST(SvgPlacement, DpiScalesUnitlessLengths) {
739
+ const auto path = std::filesystem::temp_directory_path() / "svg_place_dpi.svg";
740
+ std::ofstream(path) << R"(<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">)"
741
+ << R"(<rect x="0" y="0" width="10" height="10"/><rect x="20" y="50" width="30" height="5"/></svg>)";
742
+ const std::vector<Contour2d> contours = loadSvgContours(path.string(), {}, nullptr, 96.0, false);
743
+ double maxy = -1e18;
744
+ for (const Contour2d& ct : contours)
745
+ for (const auto& p : ct) maxy = std::max(maxy, p[1]);
746
+ EXPECT_NEAR(maxy, 26.458, 1e-3);
747
+ std::filesystem::remove(path);
748
+ }
749
+
750
+ } // namespace
751
+ } // namespace oscadeval