openscad-cpp-evaluator 1.13.0__tar.gz → 1.14.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 (219) hide show
  1. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/CLAUDE.md +18 -1
  2. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/CMakeLists.txt +20 -3
  3. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/PKG-INFO +1 -1
  4. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/bindings/module.cpp +26 -0
  5. openscad_cpp_evaluator-1.14.0/cmake/embed_font.cmake +71 -0
  6. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +13 -0
  7. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/pyproject.toml +1 -1
  8. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/python/openscad_cpp_evaluator/__init__.py +18 -1
  9. openscad_cpp_evaluator-1.14.0/resources/fonts/LiberationSans-Bold.ttf +0 -0
  10. openscad_cpp_evaluator-1.14.0/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  11. openscad_cpp_evaluator-1.14.0/resources/fonts/LiberationSans-Italic.ttf +0 -0
  12. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/function_builtins.cpp +17 -8
  13. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/text/freetype_font_provider.cpp +57 -14
  14. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_python_bindings.py +31 -0
  15. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_text.cpp +110 -0
  16. openscad_cpp_evaluator-1.13.0/cmake/embed_font.cmake +0 -28
  17. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/.github/workflows/ci.yml +0 -0
  18. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/.github/workflows/wheels.yml +0 -0
  19. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/.gitignore +0 -0
  20. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/.gitmodules +0 -0
  21. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/LICENSE +0 -0
  22. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/README.md +0 -0
  23. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/bindings/CMakeLists.txt +0 -0
  24. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/bindings/ast_to_py.cpp +0 -0
  25. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/examples/CMakeLists.txt +0 -0
  26. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/examples/eval_perf_benchmark.cpp +0 -0
  27. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/examples/manifold_cache_reuse.cpp +0 -0
  28. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/examples/minimal_debugger.cpp +0 -0
  29. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  30. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/.gitignore +0 -0
  31. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  32. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  33. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/README.md +0 -0
  34. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  35. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  36. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  37. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  38. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  39. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  40. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  41. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  42. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  43. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  44. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  45. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  46. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  47. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  48. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  49. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  50. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  51. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  52. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  53. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  54. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  55. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  56. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  57. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  58. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  59. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  60. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  61. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  62. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  63. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  64. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  65. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  66. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  67. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  68. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  69. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  70. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  71. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  72. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  73. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  74. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  75. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  76. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  77. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  78. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  79. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  80. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  81. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  82. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  83. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  84. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  85. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  86. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  87. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  88. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  89. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  90. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  91. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  92. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  93. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  94. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  95. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  96. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  97. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  98. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  99. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  100. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  101. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  102. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  103. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  104. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  105. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  106. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  107. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  108. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  109. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  110. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  111. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  112. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  113. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  114. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  115. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  116. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  117. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  118. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  119. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  120. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  121. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  122. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  123. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  124. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  125. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  126. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  127. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  128. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  129. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  130. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  131. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  132. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/resources/fonts/LICENSE +0 -0
  133. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  134. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/CMakeLists.txt +0 -0
  135. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/booleans.cpp +0 -0
  136. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/builtins.hpp +0 -0
  137. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/call_args.cpp +0 -0
  138. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/color.cpp +0 -0
  139. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/control.cpp +0 -0
  140. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/dxf_dim.cpp +0 -0
  141. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/extrude.cpp +0 -0
  142. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/import.cpp +0 -0
  143. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/modifiers.cpp +0 -0
  144. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/primitives_2d.cpp +0 -0
  145. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/primitives_3d.cpp +0 -0
  146. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/registry.cpp +0 -0
  147. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/roof.cpp +0 -0
  148. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/segments.cpp +0 -0
  149. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/surface.cpp +0 -0
  150. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/text.cpp +0 -0
  151. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/topology.cpp +0 -0
  152. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/builtins/transforms.cpp +0 -0
  153. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/bytecode_compiler.cpp +0 -0
  154. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/bytecode_vm.cpp +0 -0
  155. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/colored_body.cpp +0 -0
  156. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/csg_generate.cpp +0 -0
  157. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/csg_resolve.cpp +0 -0
  158. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/css_colors.cpp +0 -0
  159. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/debug/debug_repl.cpp +0 -0
  160. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/debug_profile.cpp +0 -0
  161. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/eval_context.cpp +0 -0
  162. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/eval_error.cpp +0 -0
  163. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/eval_use.cpp +0 -0
  164. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/export.cpp +0 -0
  165. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/expr_eval.cpp +0 -0
  166. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/format_closure.cpp +0 -0
  167. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/import/dxf_import.cpp +0 -0
  168. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/import/mesh_import.cpp +0 -0
  169. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/import/surface_load.cpp +0 -0
  170. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/import/svg_import.cpp +0 -0
  171. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/manifold_cache.cpp +0 -0
  172. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/measure_geometry.cpp +0 -0
  173. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/mesh_check.cpp +0 -0
  174. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/stmt_eval.cpp +0 -0
  175. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/text/font_match.cpp +0 -0
  176. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/text/text_metrics.cpp +0 -0
  177. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/user_calls.cpp +0 -0
  178. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/utf8.cpp +0 -0
  179. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/value.cpp +0 -0
  180. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/src/zip_stored.cpp +0 -0
  181. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/CMakeLists.txt +0 -0
  182. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_booleans.cpp +0 -0
  183. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_bytecode_compiler.cpp +0 -0
  184. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_children_separate.cpp +0 -0
  185. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_cli.cpp +0 -0
  186. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_control_flow.cpp +0 -0
  187. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_csg_tree.cpp +0 -0
  188. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_debug_hooks.cpp +0 -0
  189. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_dxf_svg_import.cpp +0 -0
  190. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_eval_context.cpp +0 -0
  191. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_export.cpp +0 -0
  192. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_expr_eval.cpp +0 -0
  193. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_extrude_roof.cpp +0 -0
  194. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_feature_detection.cpp +0 -0
  195. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_function_builtins.cpp +0 -0
  196. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_helpers.hpp +0 -0
  197. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_import_export.cpp +0 -0
  198. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_include_cache.cpp +0 -0
  199. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_manifold_cache.cpp +0 -0
  200. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_mesh_check.cpp +0 -0
  201. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_multi_color_merge.cpp +0 -0
  202. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_primitives.cpp +0 -0
  203. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_profiling.cpp +0 -0
  204. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_render_expr.cpp +0 -0
  205. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_scope_trail.cpp +0 -0
  206. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_scoping.cpp +0 -0
  207. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_surface.cpp +0 -0
  208. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_tail_calls.cpp +0 -0
  209. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_transforms.cpp +0 -0
  210. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_unexpected_args.cpp +0 -0
  211. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_use.cpp +0 -0
  212. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_value.cpp +0 -0
  213. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_viewport_params.cpp +0 -0
  214. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tests/test_zip_stored.cpp +0 -0
  215. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tools/cli/CMakeLists.txt +0 -0
  216. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tools/cli/cli_lib.cpp +0 -0
  217. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tools/cli/cli_lib.hpp +0 -0
  218. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/tools/cli/main.cpp +0 -0
  219. {openscad_cpp_evaluator-1.13.0 → openscad_cpp_evaluator-1.14.0}/uv.lock +0 -0
@@ -276,7 +276,16 @@ OpenSCAD's `detect_properties()`/`detect_script()` does by hand.
276
276
  `resolveFont()` never fails. An unmatched family falls back to the bundled Liberation Sans
277
277
  (`resources/fonts/`, embedded into the binary by `cmake/embed_font.cmake`, byte-identical to the
278
278
  Python reference's own bundled font), which is both the default and the last-resort fallback — so
279
- `text()` draws something even on a machine with no fonts installed at all. That mirrors
279
+ `text()` draws something even on a machine with no fonts installed at all.
280
+ **All four styles are bundled** (Regular, Bold, Italic, Bold Italic), and they are seeded into the
281
+ face index ahead of anything installed, so `style=` resolves the same way on every machine.
282
+ Bundling only Regular meant a `style=Bold` nothing installed could satisfy fell silently back to
283
+ Regular — `fontmetrics(10, "Liberation Sans:style=bold")` then reported Regular's numbers under
284
+ the name the script asked for (BelfrySCAD #381). A bundled entry's `path` is the sentinel
285
+ `"<bundled>"` and its `faceIndex` is the already-open handle, so `resolveFont` opens no file for it.
286
+ `FreetypeFontProvider::listFonts()` returns every resolvable face (family, style, path), sorted and
287
+ deduplicated by family+style — reached from Python as `list_fonts()`, which adds the exact `font=`
288
+ spec for each. It is the only call here that forces the system scan the rest of the class defers. That mirrors
280
289
  fontconfig's own never-fails matching, and so real OpenSCAD's: a missing font silently becomes the
281
290
  nearest available one rather than an error.
282
291
 
@@ -289,6 +298,14 @@ variable real OpenSCAD reads). Family match is case-insensitive and required; st
289
298
  dropping to Regular beats falling through to an unrelated family, which is what fontconfig does
290
299
  too.
291
300
 
301
+ **`font` is a POSITIONAL argument in both metrics functions**, and so is everything after it:
302
+ `fontmetrics(size, font)` and `textmetrics(text, size, font, direction, language, script, halign,
303
+ valign, spacing)`. Verified against the 2026.02.01 binary rather than read off a doc page. Having
304
+ `font` name-only meant `fontmetrics(10, "Liberation Sans:style=Bold")` — how the bug report was
305
+ written, and how real OpenSCAD reads it — was taken as *no font at all*, so the default face was
306
+ measured and reported under the requested name. That was the visible half of BelfrySCAD #381; the
307
+ bundled-styles gap above was the other.
308
+
292
309
  `text_metrics.hpp`/`.cpp` holds the shared layout math (`measureText`/`textAlignOffset`) used by
293
310
  both `text()` (`builtins/text.cpp`) and `textmetrics()`/`fontmetrics()` (`function_builtins.cpp`).
294
311
  Sharing it is the point: the numbers a script positions against cannot disagree with what it
@@ -218,13 +218,30 @@ FetchContent_MakeAvailable(cpp_linenoise)
218
218
  # a real installed/packaged build (e.g. the PyPI wheel) has no guarantee the
219
219
  # source tree's resources/fonts/ is present or at any predictable path
220
220
  # alongside the installed shared library.
221
- set(BUNDLED_FONT_SRC ${CMAKE_CURRENT_SOURCE_DIR}/resources/fonts/LiberationSans-Regular.ttf)
221
+ # The whole Liberation Sans family, not just Regular: a `style=` that
222
+ # cannot be satisfied silently falls back to the regular face, which is
223
+ # what made fontmetrics(10, "Liberation Sans:style=bold") report Regular's
224
+ # numbers (BelfrySCAD #381). Regular stays FIRST -- it is the default face
225
+ # and the last-resort fallback.
226
+ set(BUNDLED_FONT_SRCS
227
+ ${CMAKE_CURRENT_SOURCE_DIR}/resources/fonts/LiberationSans-Regular.ttf
228
+ ${CMAKE_CURRENT_SOURCE_DIR}/resources/fonts/LiberationSans-Bold.ttf
229
+ ${CMAKE_CURRENT_SOURCE_DIR}/resources/fonts/LiberationSans-Italic.ttf
230
+ ${CMAKE_CURRENT_SOURCE_DIR}/resources/fonts/LiberationSans-BoldItalic.ttf
231
+ )
222
232
  set(BUNDLED_FONT_HEADER ${CMAKE_CURRENT_BINARY_DIR}/generated/openscad_cpp_evaluator/bundled_font_data.hpp)
233
+ # Joined into ONE argument, and quoted. Left as a ";" list,
234
+ # add_custom_command expands it into separate arguments and the font paths
235
+ # land where -P expects the script; joined with "|" but unquoted, make
236
+ # emits a bare pipe and the shell splits the command in two. The separator
237
+ # is "@" for the same reason -- no shell meaning, and no filesystem here
238
+ # puts one in a path.
239
+ string(REPLACE ";" "@" BUNDLED_FONT_SRCS_ARG "${BUNDLED_FONT_SRCS}")
223
240
  add_custom_command(
224
241
  OUTPUT ${BUNDLED_FONT_HEADER}
225
- COMMAND ${CMAKE_COMMAND} -DIN_FILE=${BUNDLED_FONT_SRC} -DOUT_FILE=${BUNDLED_FONT_HEADER}
242
+ COMMAND ${CMAKE_COMMAND} "-DIN_FILES=${BUNDLED_FONT_SRCS_ARG}" -DOUT_FILE=${BUNDLED_FONT_HEADER}
226
243
  -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_font.cmake
227
- DEPENDS ${BUNDLED_FONT_SRC} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_font.cmake
244
+ DEPENDS ${BUNDLED_FONT_SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_font.cmake
228
245
  COMMENT "Embedding bundled font bytes into bundled_font_data.hpp"
229
246
  )
230
247
  add_custom_target(generate_bundled_font DEPENDS ${BUNDLED_FONT_HEADER})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.13.0
3
+ Version: 1.14.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -23,6 +23,7 @@
23
23
  #include "openscad_cpp_evaluator/eval_use.hpp"
24
24
  #include "openscad_cpp_evaluator/evaluator.hpp"
25
25
  #include "openscad_cpp_evaluator/export.hpp"
26
+ #include "openscad_cpp_evaluator/freetype_font_provider.hpp"
26
27
  #include "openscad_cpp_evaluator/mesh_check.hpp"
27
28
  #include "openscad_cpp_evaluator/manifold_cache.hpp"
28
29
  #include "openscad_cpp_evaluator/profile.hpp"
@@ -869,6 +870,31 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
869
870
  "add-meta-data, title, author, subject, keywords). An unknown key raises rather than "
870
871
  "being ignored. .pdf is 2D-only too, and centres the drawing on a fixed paper size.");
871
872
 
873
+ m.def("list_fonts", []() {
874
+ nb::list out;
875
+ oscadeval::FreetypeFontProvider provider;
876
+ for (const oscadeval::FontFace& f : provider.listFonts()) {
877
+ nb::dict d;
878
+ d["family"] = f.family;
879
+ d["style"] = f.style;
880
+ // The exact string to put in font=. Style included only when
881
+ // it is not the plain face: "Liberation Sans" is what you
882
+ // write for Regular, and printing the longer form for it would
883
+ // teach the wrong habit.
884
+ const bool plain = f.style.empty() || f.style == "Regular";
885
+ d["spec"] = plain ? f.family : f.family + ":style=" + f.style;
886
+ d["path"] = f.path;
887
+ out.append(d);
888
+ }
889
+ return out;
890
+ },
891
+ "Every font text()/textmetrics()/fontmetrics() can resolve: family, style, the exact "
892
+ "`font=` spec that selects it, and the file it comes from (\"<bundled>\" for the "
893
+ "faces shipped with this package). Read from the same FreeType index those functions "
894
+ "match against, NOT from the windowing toolkit -- a system font database names faces "
895
+ "differently, which is the whole reason a list is worth showing. Scans the system font "
896
+ "directories, so it costs real time on a machine with many fonts.");
897
+
872
898
  m.def("export_extensions", []() { return oscadeval::exportExtensions(); },
873
899
  "The file extensions export_model understands, dot-prefixed. Read from the C++ writer table "
874
900
  "rather than restated here, so the Python side cannot drift from what can actually be written.");
@@ -0,0 +1,71 @@
1
+ # Reads IN_FILES (a ";"-separated list of binaries) and writes OUT_FILE: a
2
+ # C++ header exposing their bytes as static arrays, plus a kBundledFonts
3
+ # table over them. Run via
4
+ # `cmake -DIN_FILES="a.ttf;b.ttf" -DOUT_FILE=... -P embed_font.cmake` as a
5
+ # build-time add_custom_command, not configure_file -- fonts baked in as
6
+ # bytes have no runtime file path to go stale once the shared library is
7
+ # installed somewhere other than its own build tree (see this script's own
8
+ # call site in the top-level CMakeLists.txt for the packaging bug this
9
+ # replaces).
10
+ #
11
+ # More than one font, because a style= that cannot be satisfied is worse
12
+ # than useless: it silently returns the regular face, which is what made
13
+ # `fontmetrics(10, "Liberation Sans:style=bold")` report Regular's numbers
14
+ # (BelfrySCAD #381). The whole family is embedded so the styles a script
15
+ # asks for exist even on a machine with no fonts of its own.
16
+ if(NOT DEFINED IN_FILES OR NOT DEFINED OUT_FILE)
17
+ message(FATAL_ERROR "embed_font.cmake requires -DIN_FILES=... -DOUT_FILE=...")
18
+ endif()
19
+
20
+ # "@"-joined by the caller into a single argument; see the call site for
21
+ # why neither a ";" list nor a "|" join survives the trip.
22
+ string(REPLACE "@" ";" IN_FILES "${IN_FILES}")
23
+
24
+ set(_decls "")
25
+ set(_table "")
26
+ set(_index 0)
27
+ foreach(_file IN LISTS IN_FILES)
28
+ file(READ "${_file}" _hex HEX)
29
+ string(LENGTH "${_hex}" _hexLen)
30
+ math(EXPR _byteCount "${_hexLen} / 2")
31
+ string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _bytesCsv "${_hex}")
32
+ get_filename_component(_name "${_file}" NAME)
33
+ string(APPEND _decls
34
+ "// ${_name}
35
+ inline constexpr unsigned char kBundledFont${_index}[] = {
36
+ ${_bytesCsv}
37
+ };
38
+ ")
39
+ string(APPEND _table " {kBundledFont${_index}, ${_byteCount}},\n")
40
+ if(_index EQUAL 0)
41
+ set(_firstSize "${_byteCount}")
42
+ endif()
43
+ math(EXPR _index "${_index} + 1")
44
+ endforeach()
45
+
46
+ file(WRITE "${OUT_FILE}"
47
+ "// Auto-generated by cmake/embed_font.cmake -- do not edit.
48
+ #pragma once
49
+ #include <cstddef>
50
+
51
+ namespace oscadeval {
52
+
53
+ ${_decls}
54
+ struct BundledFont {
55
+ const unsigned char* data;
56
+ std::size_t size;
57
+ };
58
+
59
+ // Index 0 is the default face and the last-resort fallback; the rest are
60
+ // the other styles of the same family, so a `style=` request can be met
61
+ // without anything installed on the machine.
62
+ inline constexpr BundledFont kBundledFonts[] = {
63
+ ${_table}};
64
+ inline constexpr std::size_t kBundledFontCount = ${_index};
65
+
66
+ // The default face, named for the callers that only ever wanted that one.
67
+ inline constexpr const unsigned char* kBundledFontData = kBundledFont0;
68
+ inline constexpr std::size_t kBundledFontDataSize = ${_firstSize};
69
+
70
+ } // namespace oscadeval
71
+ ")
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include "openscad_cpp_evaluator/font_match.hpp"
3
4
  #include "openscad_cpp_evaluator/font_provider.hpp"
4
5
 
5
6
  #include <memory>
@@ -30,6 +31,18 @@ public:
30
31
  std::optional<std::array<double, 4>> glyphInkBounds(FontHandle handle, GlyphId glyph) override;
31
32
  GlyphContours glyphOutline(FontHandle handle, GlyphId glyph, int segsPerCurve) override;
32
33
 
34
+ // Every face this provider can resolve: the bundled family first, then
35
+ // everything installed, sorted by family then style. Forces the system
36
+ // scan that the rest of the class defers, so it is the one call here
37
+ // that costs real time on a machine with a few hundred fonts.
38
+ //
39
+ // These are the names `font=` actually takes -- the point of showing
40
+ // them at all (BelfrySCAD #379: "the displayed font name can be
41
+ // different from the font name needed by OpenSCAD"). A list read from
42
+ // the windowing toolkit's font database would reproduce exactly that
43
+ // mismatch.
44
+ std::vector<FontFace> listFonts();
45
+
33
46
  private:
34
47
  struct Impl;
35
48
  std::unique_ptr<Impl> impl_;
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.13.0"
7
+ version = "1.14.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -20,7 +20,7 @@ __all__ = [
20
20
  "ManifoldCache", "CallSiteProfile", "ProfileResult", "format_csg_tree", "bodies_from_dicts",
21
21
  "FastContinueSignal", "parse_ast", "parse_ast_string", "format_source",
22
22
  "check_mesh", "strip_slivers",
23
- "export_model", "export_extensions",
23
+ "export_model", "export_extensions", "list_fonts",
24
24
  ]
25
25
 
26
26
 
@@ -342,6 +342,23 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
342
342
  raise EvalError(str(e)) from e
343
343
 
344
344
 
345
+ def list_fonts() -> list:
346
+ """Every font `text()`/`textmetrics()`/`fontmetrics()` can resolve.
347
+
348
+ A list of dicts with `family`, `style`, `spec` and `path`. `spec` is
349
+ the exact string to put in `font=`, which is the point of asking:
350
+ the name a system font dialog shows is often not the name OpenSCAD
351
+ takes, and this list comes from the same FreeType index the text
352
+ functions match against. `path` is `"<bundled>"` for the faces
353
+ shipped with this package.
354
+
355
+ Scans the system font directories, so it costs real time on a machine
356
+ with many fonts -- call it when someone asks to see the list, not on
357
+ a hot path.
358
+ """
359
+ return list(_ext.list_fonts())
360
+
361
+
345
362
  def export_extensions() -> list:
346
363
  """The file extensions export_model understands, dot-prefixed.
347
364
 
@@ -785,14 +785,21 @@ Value objectOf(std::vector<std::pair<std::string, Value>> items) {
785
785
  Value builtinTextmetrics(Evaluator& ev, const CallArgs& args) {
786
786
  const std::string text = asStringOr(getArg(args, 0, "text", Value{std::string("")}), "");
787
787
  const double size = toDoubleLenient(getArg(args, 1, "size", Value{10.0}));
788
- const std::string halign = asStringOr(getArg(args, std::nullopt, "halign", Value{std::string("left")}), "left");
789
- const std::string valign = asStringOr(getArg(args, std::nullopt, "valign", Value{std::string("baseline")}), "baseline");
790
- const double spacing = toDoubleLenient(getArg(args, std::nullopt, "spacing", Value{1.0}));
791
- const std::string fontSpec = asStringOr(getArg(args, std::nullopt, "font", Value{std::string("")}), "");
788
+ // Positional indices, all of them: OpenSCAD's own signature is
789
+ // textmetrics(text, size, font, direction, language, script, halign,
790
+ // valign, spacing) and it honours every one of those positionally --
791
+ // verified against the 2026.02.01 binary, not read off a doc page.
792
+ // `font` being name-only here is what made
793
+ // `textmetrics("Hi", 10, "Liberation Sans:style=Bold")` silently
794
+ // measure the default face (BelfrySCAD #381).
795
+ const std::string halign = asStringOr(getArg(args, 6, "halign", Value{std::string("left")}), "left");
796
+ const std::string valign = asStringOr(getArg(args, 7, "valign", Value{std::string("baseline")}), "baseline");
797
+ const double spacing = toDoubleLenient(getArg(args, 8, "spacing", Value{1.0}));
798
+ const std::string fontSpec = asStringOr(getArg(args, 2, "font", Value{std::string("")}), "");
792
799
  ShapeOptions shape;
793
- shape.direction = asStringOr(getArg(args, std::nullopt, "direction", Value{std::string("")}), "");
794
- shape.language = asStringOr(getArg(args, std::nullopt, "language", Value{std::string("")}), "");
795
- shape.script = asStringOr(getArg(args, std::nullopt, "script", Value{std::string("")}), "");
800
+ shape.direction = asStringOr(getArg(args, 3, "direction", Value{std::string("")}), "");
801
+ shape.language = asStringOr(getArg(args, 4, "language", Value{std::string("")}), "");
802
+ shape.script = asStringOr(getArg(args, 5, "script", Value{std::string("")}), "");
796
803
 
797
804
  FontProvider& fp = ev.fontProvider();
798
805
  const FontHandle handle = fp.resolveFont(fontSpec);
@@ -813,7 +820,9 @@ Value builtinTextmetrics(Evaluator& ev, const CallArgs& args) {
813
820
  // resolved font, scaled for `size`. Mirrors _builtin_fontmetrics.
814
821
  Value builtinFontmetrics(Evaluator& ev, const CallArgs& args) {
815
822
  const double size = toDoubleLenient(getArg(args, 0, "size", Value{10.0}));
816
- const std::string fontSpec = asStringOr(getArg(args, std::nullopt, "font", Value{std::string("")}), "");
823
+ // Positional too: fontmetrics(10, "Liberation Sans:style=Bold") is
824
+ // how the bug report was written, and how the real binary reads it.
825
+ const std::string fontSpec = asStringOr(getArg(args, 1, "font", Value{std::string("")}), "");
817
826
 
818
827
  FontProvider& fp = ev.fontProvider();
819
828
  const FontHandle handle = fp.resolveFont(fontSpec);
@@ -1,5 +1,7 @@
1
1
  #include "openscad_cpp_evaluator/freetype_font_provider.hpp"
2
2
 
3
+ #include <algorithm>
4
+
3
5
  #include "openscad_cpp_evaluator/bundled_font_data.hpp"
4
6
  #include "openscad_cpp_evaluator/font_match.hpp"
5
7
 
@@ -104,6 +106,11 @@ struct FreetypeFontProvider::Impl {
104
106
  FontMetrics metrics;
105
107
  };
106
108
 
109
+ //: A FontFace whose `path` is this is already open: its faceIndex IS
110
+ //: the handle. Real paths cannot collide with it -- no filesystem
111
+ //: yields a name in angle brackets from findFontFiles().
112
+ static constexpr const char* kBundledPath = "<bundled>";
113
+
107
114
  std::vector<Face> faces; // handle == index
108
115
  std::unordered_map<std::string, FontHandle> bySpec; // resolved spec -> handle
109
116
  std::vector<FontFace> installed; // system font index
@@ -160,21 +167,30 @@ struct FreetypeFontProvider::Impl {
160
167
  return faces.size() - 1;
161
168
  }
162
169
 
170
+ // Every bundled face, in order -- handle 0 is the regular one, the
171
+ // default and the last-resort fallback. The rest are the other styles
172
+ // of the same family: a `style=` request that nothing installed can
173
+ // satisfy used to fall silently back to regular, which is what made
174
+ // fontmetrics("Liberation Sans:style=bold") report Regular's numbers
175
+ // (BelfrySCAD #381).
163
176
  void openBundled() {
164
- Face f;
165
- if (FT_New_Memory_Face(lib, reinterpret_cast<const FT_Byte*>(kBundledFontData),
166
- static_cast<FT_Long>(kBundledFontDataSize), 0, &f.ft) != 0) {
167
- throw std::runtime_error("FreetypeFontProvider: bundled font failed to load");
177
+ for (std::size_t i = 0; i < kBundledFontCount; ++i) {
178
+ const BundledFont& src = kBundledFonts[i];
179
+ Face f;
180
+ if (FT_New_Memory_Face(lib, reinterpret_cast<const FT_Byte*>(src.data),
181
+ static_cast<FT_Long>(src.size), 0, &f.ft) != 0) {
182
+ if (i == 0) throw std::runtime_error("FreetypeFontProvider: bundled font failed to load");
183
+ continue; // a spare style is not worth failing every render for
184
+ }
185
+ f.blob = hb_blob_create(reinterpret_cast<const char*>(src.data),
186
+ static_cast<unsigned>(src.size), HB_MEMORY_MODE_READONLY, nullptr, nullptr);
187
+ f.hbFace = hb_face_create(f.blob, 0);
188
+ f.hbFont = hb_font_create(f.hbFace);
189
+ hb_ot_font_set_funcs(f.hbFont);
190
+ f.metrics = readMetrics(f.ft);
191
+ scaleHbFontToEm(f.hbFont, static_cast<unsigned>(f.metrics.unitsPerEm));
192
+ faces.push_back(f);
168
193
  }
169
- f.blob = hb_blob_create(reinterpret_cast<const char*>(kBundledFontData),
170
- static_cast<unsigned>(kBundledFontDataSize), HB_MEMORY_MODE_READONLY, nullptr,
171
- nullptr);
172
- f.hbFace = hb_face_create(f.blob, 0);
173
- f.hbFont = hb_font_create(f.hbFace);
174
- hb_ot_font_set_funcs(f.hbFont);
175
- f.metrics = readMetrics(f.ft);
176
- scaleHbFontToEm(f.hbFont, static_cast<unsigned>(f.metrics.unitsPerEm));
177
- faces.push_back(f);
178
194
  }
179
195
 
180
196
  // Reads family/style out of every face of every installed font file.
@@ -185,6 +201,15 @@ struct FreetypeFontProvider::Impl {
185
201
  void scanInstalled() {
186
202
  if (scanned) return;
187
203
  scanned = true;
204
+ // The bundled faces go in FIRST, so what a given family+style
205
+ // resolves to does not depend on what the machine happens to have
206
+ // installed -- the same script draws the same shapes everywhere.
207
+ // Their "path" is the sentinel below and their faceIndex is the
208
+ // already-open handle, so resolveFont skips opening a file at all.
209
+ for (std::size_t i = 0; i < faces.size() && i < kBundledFontCount; ++i) {
210
+ installed.push_back(FontFace{std::string(kBundledPath), static_cast<int>(i),
211
+ faces[i].metrics.family, faces[i].metrics.style});
212
+ }
188
213
  for (const std::string& path : findFontFiles()) {
189
214
  FT_Face probe = nullptr;
190
215
  if (FT_New_Face(lib, path.c_str(), -1, &probe) != 0) continue;
@@ -222,12 +247,30 @@ FontHandle FreetypeFontProvider::resolveFont(const std::string& spec) {
222
247
  impl_->scanInstalled();
223
248
  FontHandle handle = 0;
224
249
  if (const std::optional<FontFace> hit = matchFace(parsed, impl_->installed)) {
225
- handle = impl_->openFile(hit->path, hit->faceIndex).value_or(0);
250
+ handle = hit->path == Impl::kBundledPath ? static_cast<FontHandle>(hit->faceIndex)
251
+ : impl_->openFile(hit->path, hit->faceIndex).value_or(0);
226
252
  }
227
253
  impl_->bySpec[spec] = handle;
228
254
  return handle;
229
255
  }
230
256
 
257
+ std::vector<FontFace> FreetypeFontProvider::listFonts() {
258
+ impl_->scanInstalled();
259
+ std::vector<FontFace> out = impl_->installed;
260
+ // A family installed in several files, or listed both bundled and
261
+ // installed, should appear once per STYLE, not once per file.
262
+ std::sort(out.begin(), out.end(), [](const FontFace& a, const FontFace& b) {
263
+ if (a.family != b.family) return a.family < b.family;
264
+ return a.style < b.style;
265
+ });
266
+ out.erase(std::unique(out.begin(), out.end(),
267
+ [](const FontFace& a, const FontFace& b) {
268
+ return a.family == b.family && a.style == b.style;
269
+ }),
270
+ out.end());
271
+ return out;
272
+ }
273
+
231
274
  FontMetrics FreetypeFontProvider::metrics(FontHandle handle) {
232
275
  if (handle >= impl_->faces.size()) return impl_->faces[0].metrics;
233
276
  return impl_->faces[handle].metrics;
@@ -745,6 +745,37 @@ def test_export_model_rejects_an_unknown_pdf_option(tmp_path):
745
745
  export_model(str(tmp_path / "x.pdf"), ev.geometry, pdf_options={"paper-size": "a2"})
746
746
 
747
747
 
748
+ def test_list_fonts_reports_specs_that_actually_resolve(tmp_path):
749
+ """The list exists so a name can be pasted into font=.
750
+
751
+ Read from the evaluator's own FreeType index, not the windowing
752
+ toolkit's -- a system font database names faces differently, which is
753
+ the whole reason BelfrySCAD #379 asked for a list.
754
+ """
755
+ from openscad_cpp_evaluator import Evaluator, list_fonts
756
+
757
+ fonts = list_fonts()
758
+ assert fonts, "no fonts at all, not even the bundled ones"
759
+ for f in fonts:
760
+ assert set(f) == {"family", "style", "spec", "path"}
761
+
762
+ bundled = {f["style"]: f for f in fonts if f["family"] == "Liberation Sans"}
763
+ assert {"Regular", "Bold", "Italic", "Bold Italic"} <= set(bundled)
764
+ # Regular is written as the bare family: printing the longer form for
765
+ # it would teach the wrong habit.
766
+ assert bundled["Regular"]["spec"] == "Liberation Sans"
767
+ assert bundled["Bold"]["spec"] == "Liberation Sans:style=Bold"
768
+
769
+ # ... and every spec resolves to the face it names.
770
+ src = tmp_path / "fonts.scad"
771
+ src.write_text("".join(
772
+ f'echo(fontmetrics(10, "{bundled[style]["spec"]}").font.style);\n'
773
+ for style in ("Regular", "Bold", "Italic", "Bold Italic")))
774
+ echoes = []
775
+ Evaluator(echo_fn=echoes.append).evaluate(str(src), {})
776
+ assert echoes == ['ECHO: "Regular"', 'ECHO: "Bold"', 'ECHO: "Italic"', 'ECHO: "Bold Italic"']
777
+
778
+
748
779
  def test_strict_commas_reaches_the_parser(tmp_path):
749
780
  """Evaluator.evaluate(strict_commas=True) must survive both hand-written
750
781
  parameter lists -- the facade's and the nanobind binding's. The last
@@ -1,4 +1,8 @@
1
1
  #include "openscad_cpp_evaluator/evaluator.hpp"
2
+ #include "openscad_cpp_evaluator/freetype_font_provider.hpp"
3
+
4
+ #include <algorithm>
5
+ #include <array>
2
6
  #include "openscad_cpp_evaluator/font_match.hpp"
3
7
 
4
8
  #include "test_helpers.hpp"
@@ -269,3 +273,109 @@ TEST(FontMatch, StyleIsAPreferenceNotAFilter) {
269
273
  ASSERT_TRUE(hit.has_value());
270
274
  EXPECT_EQ(hit->style, "Regular");
271
275
  }
276
+
277
+ // -- bundled family + font list (BelfrySCAD #381, #379) -------------------
278
+
279
+ TEST(BundledFonts, EveryStyleOfTheFamilyResolves) {
280
+ // Only Regular used to be bundled, so `style=Bold` on a machine
281
+ // without Liberation installed fell silently back to Regular --
282
+ // fontmetrics() then reported Regular's numbers under the name the
283
+ // script asked for, which is what #381 saw.
284
+ FreetypeFontProvider p;
285
+ const std::array<std::pair<const char*, const char*>, 4> want{{
286
+ {"Liberation Sans", "Regular"},
287
+ {"Liberation Sans:style=Bold", "Bold"},
288
+ {"Liberation Sans:style=Italic", "Italic"},
289
+ {"Liberation Sans:style=Bold Italic", "Bold Italic"},
290
+ }};
291
+ for (const auto& [spec, style] : want) {
292
+ const FontMetrics m = p.metrics(p.resolveFont(spec));
293
+ EXPECT_EQ(m.family, "Liberation Sans") << spec;
294
+ EXPECT_EQ(m.style, style) << spec;
295
+ }
296
+ }
297
+
298
+ TEST(BundledFonts, StyleMatchingIsCaseInsensitive) {
299
+ // The report used lowercase "bold", as fontconfig accepts.
300
+ FreetypeFontProvider p;
301
+ EXPECT_EQ(p.metrics(p.resolveFont("Liberation Sans:style=bold")).style, "Bold");
302
+ EXPECT_EQ(p.metrics(p.resolveFont("Liberation Sans:style=BOLD")).style, "Bold");
303
+ }
304
+
305
+ TEST(BundledFonts, EachStyleHasItsOwnMetrics) {
306
+ // The visible half of #381: `max` ascent/descent came back identical
307
+ // for bold and regular, because they were the same face.
308
+ FreetypeFontProvider p;
309
+ const FontMetrics reg = p.metrics(p.resolveFont("Liberation Sans"));
310
+ const FontMetrics bold = p.metrics(p.resolveFont("Liberation Sans:style=Bold"));
311
+ EXPECT_NE(reg.yMax, bold.yMax);
312
+ EXPECT_NE(reg.yMin, bold.yMin);
313
+ }
314
+
315
+ TEST(BundledFonts, AnUnknownFamilyStillFallsBackRatherThanFailing) {
316
+ // A render must not die because a font is missing.
317
+ FreetypeFontProvider p;
318
+ const FontMetrics m = p.metrics(p.resolveFont("No Such Font At All"));
319
+ EXPECT_EQ(m.family, "Liberation Sans");
320
+ }
321
+
322
+ TEST(ListFonts, ReportsTheBundledFamilyWithEveryStyle) {
323
+ FreetypeFontProvider p;
324
+ const std::vector<FontFace> faces = p.listFonts();
325
+ std::vector<std::string> styles;
326
+ for (const FontFace& f : faces) {
327
+ if (f.family == "Liberation Sans") styles.push_back(f.style);
328
+ }
329
+ for (const char* want : {"Regular", "Bold", "Italic", "Bold Italic"}) {
330
+ EXPECT_NE(std::find(styles.begin(), styles.end(), want), styles.end()) << want;
331
+ }
332
+ }
333
+
334
+ TEST(ListFonts, IsSortedAndHasNoDuplicateFamilyStylePairs) {
335
+ // A family installed in several files, or bundled AND installed, must
336
+ // appear once per style -- the list is for reading.
337
+ FreetypeFontProvider p;
338
+ const std::vector<FontFace> faces = p.listFonts();
339
+ ASSERT_FALSE(faces.empty());
340
+ for (size_t i = 1; i < faces.size(); ++i) {
341
+ const bool ordered = faces[i - 1].family < faces[i].family ||
342
+ (faces[i - 1].family == faces[i].family && faces[i - 1].style < faces[i].style);
343
+ EXPECT_TRUE(ordered) << faces[i - 1].family << "/" << faces[i - 1].style << " then "
344
+ << faces[i].family << "/" << faces[i].style;
345
+ }
346
+ }
347
+
348
+ TEST(FontArgs, FontIsPositionalInBothMetricsFunctions) {
349
+ // fontmetrics(size, font) and
350
+ // textmetrics(text, size, font, direction, language, script, halign,
351
+ // valign, spacing) -- every one positional, verified against OpenSCAD
352
+ // 2026.02.01 itself. `font` used to be name-only, so the spec in the
353
+ // bug report was read as no font at all and the default face measured
354
+ // instead (BelfrySCAD #381).
355
+ std::vector<std::string> echoes;
356
+ Evaluated e = evalSrc(
357
+ "a = fontmetrics(10, \"Liberation Sans:style=Bold\").font;\n"
358
+ "b = fontmetrics(size=10, font=\"Liberation Sans:style=Bold\").font;\n"
359
+ "c = textmetrics(\"Hi\", 10, \"Liberation Sans:style=Bold\").size;\n"
360
+ "d = textmetrics(text=\"Hi\", size=10, font=\"Liberation Sans:style=Bold\").size;\n"
361
+ "e = textmetrics(\"Hi\", 10).size;\n"
362
+ "echo(a.style, b.style, c == d, c == e);\n", [&](const std::string& m) { echoes.push_back(m); });
363
+ ASSERT_EQ(echoes.size(), 1u);
364
+ // Bold both ways, and the positional form measures the same as the
365
+ // named one -- while differing from the default face.
366
+ EXPECT_EQ(echoes[0], "ECHO: \"Bold\", \"Bold\", true, false");
367
+ }
368
+
369
+ TEST(FontArgs, TheLaterPositionalsLineUpToo) {
370
+ // halign at 6 rather than "wherever": a positional font that shifted
371
+ // everything after it would be a new bug in place of the old one.
372
+ std::vector<std::string> echoes;
373
+ Evaluated e = evalSrc(
374
+ "p = textmetrics(\"Hi\", 10, \"Liberation Sans\", \"ltr\", \"en\", \"latin\", \"center\").position;\n"
375
+ "n = textmetrics(\"Hi\", 10, \"Liberation Sans\", halign=\"center\").position;\n"
376
+ "s = textmetrics(\"Hi\", 10, \"Liberation Sans\", \"ltr\", \"en\", \"latin\", \"left\", \"baseline\", 2).size;\n"
377
+ "sn = textmetrics(\"Hi\", 10, \"Liberation Sans\", spacing=2).size;\n"
378
+ "echo(p == n, s == sn);\n", [&](const std::string& m) { echoes.push_back(m); });
379
+ ASSERT_EQ(echoes.size(), 1u);
380
+ EXPECT_EQ(echoes[0], "ECHO: true, true");
381
+ }
@@ -1,28 +0,0 @@
1
- # Reads IN_FILE (binary) and writes OUT_FILE: a C++ header exposing its
2
- # bytes as a static array (kBundledFontData/kBundledFontDataSize). Run via
3
- # `cmake -DIN_FILE=... -DOUT_FILE=... -P embed_font.cmake` as a build-time
4
- # add_custom_command, not configure_file -- a font baked in as bytes has no
5
- # runtime file path to go stale once the shared library is installed
6
- # somewhere other than its own build tree (see this script's own call site
7
- # in the top-level CMakeLists.txt for the packaging bug this replaces).
8
- if(NOT DEFINED IN_FILE OR NOT DEFINED OUT_FILE)
9
- message(FATAL_ERROR "embed_font.cmake requires -DIN_FILE=... -DOUT_FILE=...")
10
- endif()
11
-
12
- file(READ "${IN_FILE}" _hex HEX)
13
- string(LENGTH "${_hex}" _hexLen)
14
- math(EXPR _byteCount "${_hexLen} / 2")
15
- string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _bytesCsv "${_hex}")
16
-
17
- file(WRITE "${OUT_FILE}"
18
- "// Auto-generated by cmake/embed_font.cmake from ${IN_FILE} -- do not edit.
19
- #pragma once
20
- #include <cstddef>
21
-
22
- namespace oscadeval {
23
- inline constexpr unsigned char kBundledFontData[] = {
24
- ${_bytesCsv}
25
- };
26
- inline constexpr std::size_t kBundledFontDataSize = ${_byteCount};
27
- } // namespace oscadeval
28
- ")