openscad-cpp-evaluator 1.14.0__tar.gz → 1.18.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.
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/CLAUDE.md +71 -1
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/PKG-INFO +1 -1
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/bindings/module.cpp +21 -12
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/colored_body.hpp +43 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_use.hpp +12 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/evaluator.hpp +30 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/export.hpp +8 -1
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/scope_trail.hpp +40 -4
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/pyproject.toml +1 -1
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/python/openscad_cpp_evaluator/__init__.py +13 -3
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/booleans.cpp +32 -3
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/color.cpp +2 -2
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/control.cpp +1 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/text.cpp +12 -1
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/bytecode_vm.cpp +1 -1
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/csg_generate.cpp +22 -2
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/csg_resolve.cpp +2 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/eval_use.cpp +6 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/export.cpp +16 -4
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/expr_eval.cpp +35 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/user_calls.cpp +6 -2
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_booleans.cpp +27 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_export.cpp +60 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_include_cache.cpp +2 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_manifold_cache.cpp +7 -8
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_python_bindings.py +21 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_scope_trail.cpp +64 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_text.cpp +27 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_use.cpp +56 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/cli_lib.cpp +1 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.github/workflows/ci.yml +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.github/workflows/wheels.yml +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.gitignore +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/.gitmodules +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/LICENSE +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/README.md +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/bindings/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/bindings/ast_to_py.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/cmake/embed_font.cmake +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/eval_perf_benchmark.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/manifold_cache_reuse.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/examples/minimal_debugger.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/.gitignore +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/README.md +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LICENSE +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/builtins.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/call_args.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/dxf_dim.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/extrude.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/function_builtins.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/import.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/modifiers.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/primitives_2d.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/primitives_3d.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/registry.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/roof.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/segments.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/surface.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/topology.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/transforms.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/bytecode_compiler.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/colored_body.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/css_colors.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/debug/debug_repl.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/debug_profile.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/eval_context.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/eval_error.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/format_closure.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/dxf_import.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/mesh_import.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/surface_load.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/import/svg_import.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/manifold_cache.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/measure_geometry.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/mesh_check.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/stmt_eval.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/text/font_match.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/text/freetype_font_provider.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/text/text_metrics.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/utf8.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/value.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/src/zip_stored.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_bytecode_compiler.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_children_separate.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_cli.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_control_flow.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_csg_tree.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_debug_hooks.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_dxf_svg_import.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_eval_context.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_expr_eval.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_extrude_roof.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_feature_detection.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_function_builtins.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_helpers.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_import_export.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_mesh_check.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_multi_color_merge.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_primitives.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_profiling.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_render_expr.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_scoping.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_surface.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_tail_calls.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_transforms.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_unexpected_args.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_value.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_viewport_params.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tests/test_zip_stored.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/cli_lib.hpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/main.cpp +0 -0
- {openscad_cpp_evaluator-1.14.0 → openscad_cpp_evaluator-1.18.0}/uv.lock +0 -0
|
@@ -298,6 +298,13 @@ variable real OpenSCAD reads). Family match is case-insensitive and required; st
|
|
|
298
298
|
dropping to Regular beats falling through to an unrelated family, which is what fontconfig does
|
|
299
299
|
too.
|
|
300
300
|
|
|
301
|
+
**`text()` takes `font` positionally too — and NOTHING after it.** `text(text, size, font)` is the
|
|
302
|
+
positional set; `halign`/`valign`/`spacing`/`direction`/`language`/`script` are name-only, because
|
|
303
|
+
that is what the 2026.02.01 binary does: `text("Hi", 10, "F", "center")` is not centred there while
|
|
304
|
+
`halign="center"` is, measured by bounding box. So **`text()` and `textmetrics()` genuinely differ**
|
|
305
|
+
— the latter takes all nine positionally — and neither can be assumed from the other. `font` being
|
|
306
|
+
name-only made `text("Hi", 10, "Liberation Sans:style=Bold")` silently draw the default face.
|
|
307
|
+
|
|
301
308
|
**`font` is a POSITIONAL argument in both metrics functions**, and so is everything after it:
|
|
302
309
|
`fontmetrics(size, font)` and `textmetrics(text, size, font, direction, language, script, halign,
|
|
303
310
|
valign, spacing)`. Verified against the 2026.02.01 binary rather than read off a doc page. Having
|
|
@@ -442,6 +449,39 @@ closed in one pass after a fresh "anything still unimplemented?" audit. Each bel
|
|
|
442
449
|
all (`resolve_use_scopes`/`UseStatement` had zero hits in `openscad_evaluator`'s own `tests/`), so
|
|
443
450
|
these were verified against real reference *behavior*, not ported from an existing test.
|
|
444
451
|
|
|
452
|
+
**File-level globals are evaluated ONCE per run (v1.16.0).** The main file's and every
|
|
453
|
+
`include`'s are, by the top-level statement pass, into the root `let_` level; a `use`d file's
|
|
454
|
+
are not -- `use` injects only declarations -- so `Evaluator::fileGlobal` evaluates ALL of that
|
|
455
|
+
file's top-level assignments, in source order, into a context of its own the first time anything
|
|
456
|
+
in the file reads one, and keeps it for the run. `ResolvedUseScopes::usedFileGlobals` carries
|
|
457
|
+
the per-file assignment lists and every entry point hands them over with
|
|
458
|
+
`Evaluator::setUsedFileGlobals` (CLI, both binding paths, the tests); a caller that skips that
|
|
459
|
+
keeps the old per-read fallback. Before this, `evalIdentifier`'s scope fallback re-evaluated the
|
|
460
|
+
assignment's expression on EVERY read from inside a function -- `callCtx()` isolates `let_` so the
|
|
461
|
+
root level was invisible -- which made `r = rands(0,1,1); function g() = r;` give `g() != g()`
|
|
462
|
+
(OpenSCAD: equal) and a 20K-element global list read 2000 times from a function cost 9.7s
|
|
463
|
+
(now 0.02s). Two deliberate divergences from OpenSCAD 2026.02.01, both verified against it:
|
|
464
|
+
a used file's globals are evaluated once, not once per call into the file (upstream's per-call
|
|
465
|
+
re-evaluation is a long-standing bug there, so its forward-read warning prints once here, not
|
|
466
|
+
per call); and a global read from a function BEFORE its assignment has run is `undef` with an
|
|
467
|
+
"Ignoring unknown variable" warning, exactly as upstream, where the old fallback had quietly
|
|
468
|
+
computed the not-yet-assigned value.
|
|
469
|
+
**Scope-trail level table shrinks (v1.18.0).** `ScopeTrailStorage::parent_` and its indexed twin
|
|
470
|
+
map level -> parent level in a vector indexed by level. Levels came from `++nextLevel_` and were
|
|
471
|
+
never reused, so the vector grew 4 bytes per level for the whole run -- 3 x 64MB on a 13M-call
|
|
472
|
+
script, plus the copy at every doubling -- and was 192MB of fractal_tree.scad's peak. A popped
|
|
473
|
+
level is now marked -1 (0 still means "live, no parent": an isolated root), and when the NEWEST
|
|
474
|
+
level dies `markDead` pops the vector back past every dead level above the newest live one, whose
|
|
475
|
+
numbers `openLevel` then hands out again. Every invariant `lookup()`'s sorted-merge walk relies on
|
|
476
|
+
holds: a reused number is greater than every level still holding an entry (all higher levels were
|
|
477
|
+
dead and `popLevel` had already removed their entries), so a name's push-stack stays ascending,
|
|
478
|
+
and a live level's number never changes -- an escaping closure that keeps a level alive simply
|
|
479
|
+
stops the shrink at it (`tests/test_scope_trail.cpp` covers both). The ancestry walk now stops at
|
|
480
|
+
`ancestor > 0` rather than `!= 0`. Same PR: `callCtxFor`'s per-call scan over active declarations
|
|
481
|
+
does the integer span test before the origin STRING compare (`_platform_memcmp` was 1.4% of
|
|
482
|
+
fractal_tree), and driveVm's completion branch no longer copies the finished frame's name. Anklet
|
|
483
|
+
74 -> 63MB, a 4M-call synthetic 380 -> 258MB, fractal_tree -3.4% wall.
|
|
484
|
+
|
|
445
485
|
**Known gap closed in Phase 3, worth remembering for Phase 5+**: every builtin resolve function must
|
|
446
486
|
call `resolveCallArgs` (call_args.hpp), never bare `resolveArgs`, or a `$`-prefixed named call
|
|
447
487
|
argument (`circle(r=2, $fn=64)`) silently fails to reach `ctx.dyn`. Caught by a geometric-correctness
|
|
@@ -903,6 +943,28 @@ grep for `ponytail:`.
|
|
|
903
943
|
operation`. `linear_extrude`/`rotate_extrude`/`offset` are always-2D and `projection` always-3D, so
|
|
904
944
|
they skip the "mixing" line and emit only the second; `roof()` emits neither, matching the
|
|
905
945
|
reference. Background (`%`) children are exempt.
|
|
946
|
+
**The emptiness test is cached on the body (`ColoredBody::knownEmpty`, v1.17.0).** It used to
|
|
947
|
+
call `Manifold::NumTri()` on every child at every level, and every Manifold accessor goes through
|
|
948
|
+
`GetImpl()`, which materialises a lazy transform (a full mesh + collider copy) and runs a lazy
|
|
949
|
+
boolean -- so each leaf was copied once per ancestor transform, and because every node kept its
|
|
950
|
+
`bodies` until the tree died, all those copies stayed alive: fractal_tree.scad held 72,929
|
|
951
|
+
transformed copies of 1024 leaves (3.1GB). Now a body is asked once, the flag rides along on the
|
|
952
|
+
struct copies transform/colour/flatten make (emptiness is transform-invariant, and every generate
|
|
953
|
+
function that builds a new body starts from a fresh `ColoredBody`), and `generateTreeImpl` clears
|
|
954
|
+
each child's `bodies` once the parent has consumed them (move-assigning a fresh vector: `clear()`
|
|
955
|
+
and `= {}` both keep the capacity, and 35K nodes x 592-byte slots was 230MB) -- only top-level
|
|
956
|
+
bodies leave, a cache hit re-fills from the cache, and the bindings' `csg_tree` facade no longer
|
|
957
|
+
converts per-node bodies (it used to `GetMeshGL()` every node into numpy on every evaluate(), for
|
|
958
|
+
a dump that never read them). Three more forcing points had to go for the chain to stay lazy: the
|
|
959
|
+
booleans' `Status()` operand filter (now `bodyStatus()`, same cache), the boolean RESULT's own
|
|
960
|
+
emptiness (derived from the operands' flags -- a union is non-empty if any operand is -- rather
|
|
961
|
+
than asked), and `attachTriColors`, which `GetMeshGL()`'d every boolean result and now runs only
|
|
962
|
+
when the operands could actually disagree on colour. `Evaluator::generatedNodeCount` replaced
|
|
963
|
+
"does the child have bodies" as the cache-hit proxy in tests. Transforms compose lazily inside
|
|
964
|
+
Manifold and each leaf is transformed once, at the boolean that needs it. fractal_tree 3.1GB ->
|
|
965
|
+
0.67GB, Dalek 370 -> 277MB, snappy-reprap full_assembly 1.6GB -> 0.87GB; wall time neutral to
|
|
966
|
+
-3% interleaved. The output is geometrically identical (triangle count, area, volume) but not
|
|
967
|
+
byte-identical: Manifold evaluates one flat batch union instead of our old level-by-level order.
|
|
906
968
|
|
|
907
969
|
Doing this centrally is also what keeps `generateCsg` safe: it builds ONE result and switches on
|
|
908
970
|
whether it holds a `body` or a `section`, so a group that changed dimension part-way dereferenced
|
|
@@ -970,7 +1032,15 @@ grep for `ponytail:`.
|
|
|
970
1032
|
restating the list, because three hand-written copies had already drifted far enough that `.off`
|
|
971
1033
|
could be written by neither the GUI nor the CLI while every test passed.
|
|
972
1034
|
Colour is carried by 3MF, AMF, OBJ (companion `.mtl`), PLY, VRML and X3D; STL and OFF compose
|
|
973
|
-
every body into one solid and store geometry only.
|
|
1035
|
+
every body into one solid and store geometry only.
|
|
1036
|
+
**`ExportOptions::splitColors` picks what the file is FOR**, and is the axis worth asking a user
|
|
1037
|
+
about rather than "should parts be separate": on (the default) writes one object per colour, which
|
|
1038
|
+
is what a **multi-material** print wants — each colour is something the slicer assigns to a
|
|
1039
|
+
filament. Off is a **single-material** print: everything unions into one welded solid, so touching
|
|
1040
|
+
colour regions lose the coincident faces that stacking per-colour objects would leave, and the
|
|
1041
|
+
per-triangle colour is dropped because such a print has no use for it. Implemented by forcing
|
|
1042
|
+
`splitBodiesForExport`'s `allSame` branch, which already unions rather than concatenates —
|
|
1043
|
+
concatenating the per-colour objects instead would have left the seam. **AMF** puts colour on the `<volume>` rather
|
|
974
1044
|
than the face, so an object whose triangles are not all one colour is written as one volume per
|
|
975
1045
|
colour — per-triangle `<color>` is legal in the spec but poorly supported by the slicers that are
|
|
976
1046
|
the audience for it.
|
|
@@ -223,8 +223,9 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
|
|
|
223
223
|
return d;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
// CSGNode -> a facade `_CSGNode` (kind/params/
|
|
227
|
-
// recursively, eagerly converting every field
|
|
226
|
+
// CSGNode -> a facade `_CSGNode` (kind/params/is_builtin/children; `bodies`
|
|
227
|
+
// is always empty, see below), recursively, eagerly converting every field
|
|
228
|
+
// to plain Python data. No
|
|
228
229
|
// lifetime extension needed -- CSGParams (Value-only, see csg_node.hpp) and
|
|
229
230
|
// ColoredBody never hold AST pointers, so the result is fully self-contained
|
|
230
231
|
// once built, unlike CSGNode::node itself (deliberately not exposed: the AST
|
|
@@ -234,15 +235,14 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
|
|
|
234
235
|
nb::object csgNodeToPy(const oscadeval::CSGNode& node) {
|
|
235
236
|
nb::dict params;
|
|
236
237
|
for (const auto& [k, v] : node.params) params[nb::str(k.c_str())] = valueToPy(v);
|
|
238
|
+
// Deliberately no bodies. This used to GetMeshGL() every body of every
|
|
239
|
+
// node in the tree into numpy on every evaluate() -- and the only
|
|
240
|
+
// consumer, format_csg_tree(), never read them. Since v1.17.0 an inner
|
|
241
|
+
// node's bodies are released once its parent has consumed them (see
|
|
242
|
+
// generateTreeImpl), so there is nothing here to convert anyway; and a
|
|
243
|
+
// body the tree still holds may be a LAZY Manifold whose GetMeshGL()
|
|
244
|
+
// would materialise the very transform that release exists to avoid.
|
|
237
245
|
nb::list bodies;
|
|
238
|
-
for (const oscadeval::ColoredBody& cb : node.bodies) {
|
|
239
|
-
if (!cb.isDisplayOnly() && (!cb.body || cb.body->IsEmpty())) continue;
|
|
240
|
-
// ColoredBody here is const in this walk (tree ownership stays with
|
|
241
|
-
// the caller) -- GetMeshGL() itself is logically read-only, so a
|
|
242
|
-
// const_cast here is safe/local, mirroring bodyToDict's mutating
|
|
243
|
-
// signature only because manifold3d's own API isn't const-qualified.
|
|
244
|
-
bodies.append(bodyToDict(const_cast<oscadeval::ColoredBody&>(cb)));
|
|
245
|
-
}
|
|
246
246
|
nb::list children;
|
|
247
247
|
for (const std::unique_ptr<oscadeval::CSGNode>& child : node.children) children.append(csgNodeToPy(*child));
|
|
248
248
|
return facadeAttr("_CSGNode")(node.kind, params, bodies, node.isBuiltin, children);
|
|
@@ -404,7 +404,8 @@ void applyPdfOptions(const nb::dict& d, oscadeval::ExportPdfOptions& pdf) {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
nb::list exportModelPy(const std::string& path, const Geometry& geom, const std::string& format, bool asciiStl,
|
|
407
|
-
bool stripSlivers, bool splitComponents, bool
|
|
407
|
+
bool stripSlivers, bool splitComponents, bool splitColors, bool svgFill,
|
|
408
|
+
const std::string& svgFillColor,
|
|
408
409
|
bool svgStroke, const std::string& svgStrokeColor, double svgStrokeWidth,
|
|
409
410
|
nb::object pdfOptions) {
|
|
410
411
|
oscadeval::ExportPdfOptions pdf;
|
|
@@ -418,6 +419,7 @@ nb::list exportModelPy(const std::string& path, const Geometry& geom, const std:
|
|
|
418
419
|
opts.asciiStl = asciiStl;
|
|
419
420
|
opts.stripSlivers = stripSlivers;
|
|
420
421
|
opts.splitComponents = splitComponents;
|
|
422
|
+
opts.splitColors = splitColors;
|
|
421
423
|
opts.svg.fill = svgFill;
|
|
422
424
|
opts.svg.fillColor = svgFillColor;
|
|
423
425
|
opts.svg.stroke = svgStroke;
|
|
@@ -455,6 +457,7 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
|
|
|
455
457
|
oscad::ParsedProgram program = oscad::getProgramFromFile(path);
|
|
456
458
|
oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, logFn);
|
|
457
459
|
oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile);
|
|
460
|
+
ev.setUsedFileGlobals(used.usedFileGlobals);
|
|
458
461
|
oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
|
|
459
462
|
bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp, generate));
|
|
460
463
|
collectIdSpans(ev, idSpans);
|
|
@@ -762,6 +765,7 @@ nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::c
|
|
|
762
765
|
oscadeval::Evaluator ev(echoCpp, nullptr, manifoldCache, hooks, false);
|
|
763
766
|
evPtr = &ev;
|
|
764
767
|
if (fastContinueSignal) ev.setFastContinueInterruptFlag(fastContinueSignal->flag());
|
|
768
|
+
ev.setUsedFileGlobals(used.usedFileGlobals);
|
|
765
769
|
oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
|
|
766
770
|
bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp));
|
|
767
771
|
collectIdSpans(ev, idSpans);
|
|
@@ -850,7 +854,8 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
|
|
|
850
854
|
|
|
851
855
|
m.def("export_model", &exportModelPy, nb::arg("path"), nb::arg("geometry"), nb::arg("format") = std::string(),
|
|
852
856
|
nb::arg("ascii_stl") = false, nb::arg("strip_slivers") = true,
|
|
853
|
-
nb::arg("split_components") = false, nb::arg("
|
|
857
|
+
nb::arg("split_components") = false, nb::arg("split_colors") = true,
|
|
858
|
+
nb::arg("svg_fill") = false,
|
|
854
859
|
nb::arg("svg_fill_color") = std::string("white"), nb::arg("svg_stroke") = true,
|
|
855
860
|
nb::arg("svg_stroke_color") = std::string("black"), nb::arg("svg_stroke_width") = 0.35,
|
|
856
861
|
nb::arg("pdf_options") = nb::none(),
|
|
@@ -861,6 +866,10 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
|
|
|
861
866
|
"`split_components` gives every disconnected piece its own object in the multi-object formats "
|
|
862
867
|
"(3MF, AMF, OBJ, PLY, VRML, X3D). Off by default, matching OpenSCAD, which writes one object "
|
|
863
868
|
"per colour however many pieces it is in.\n\n"
|
|
869
|
+
"`split_colors` (on by default) writes one object per colour -- what a MULTI-MATERIAL print "
|
|
870
|
+
"wants, each colour being something the slicer assigns to a filament. Turn it off for a "
|
|
871
|
+
"single-material print: everything welds into one solid, with no seams between colour "
|
|
872
|
+
"regions and no per-triangle colour it has no use for.\n\n"
|
|
864
873
|
"The svg_* arguments are OpenSCAD's -O export-svg/... set and apply to .svg only. "
|
|
865
874
|
"svg_stroke/svg_stroke_width also pad the page, so they are not purely cosmetic. An .svg "
|
|
866
875
|
"export needs an all-2D model and raises otherwise, as OpenSCAD does.\n\n"
|
|
@@ -53,6 +53,22 @@ struct ColoredBody {
|
|
|
53
53
|
BodyRole role = BodyRole::Normal;
|
|
54
54
|
std::optional<std::vector<std::array<float, 4>>> triColors; // per-triangle RGBA, multi-color CSG merges only
|
|
55
55
|
|
|
56
|
+
// Whether `body` is empty, once anything has asked (isEmptyBody,
|
|
57
|
+
// csg_generate.cpp). Asking Manifold is not free: every accessor goes
|
|
58
|
+
// through GetImpl(), which MATERIALIZES a lazy transform (a full copy of
|
|
59
|
+
// the mesh and its collider) and, for a boolean, runs it. Emptiness is
|
|
60
|
+
// invariant under transforms and colour, and every generate function
|
|
61
|
+
// that produces a genuinely new body starts from a fresh ColoredBody,
|
|
62
|
+
// so a copy carrying this flag is always still right. Before this, the
|
|
63
|
+
// dimension check at every tree level materialised every child at
|
|
64
|
+
// every level: fractal_tree.scad held 72,929 transformed copies of its
|
|
65
|
+
// 1024 leaf meshes at once (3.1GB).
|
|
66
|
+
std::optional<bool> knownEmpty;
|
|
67
|
+
// Same idea for Manifold::Status(), which the booleans consult to keep an
|
|
68
|
+
// invalid operand from poisoning a union: validity is transform-invariant
|
|
69
|
+
// too, and a boolean of valid operands is valid.
|
|
70
|
+
std::optional<manifold::Manifold::Error> knownStatus;
|
|
71
|
+
|
|
56
72
|
// Set ONLY when a mesh could not be built into a valid Manifold -- in
|
|
57
73
|
// practice a polyhedron() whose faces don't close the surface (it has
|
|
58
74
|
// boundary edges), which Manifold reports by returning an *empty* body
|
|
@@ -75,6 +91,33 @@ struct ColoredBody {
|
|
|
75
91
|
bool isDisplayOnly() const { return rawMesh.has_value(); }
|
|
76
92
|
};
|
|
77
93
|
|
|
94
|
+
// The cached questions -- see ColoredBody::knownEmpty. Ask Manifold once per
|
|
95
|
+
// body, never once per tree level.
|
|
96
|
+
inline bool bodyIsEmpty(ColoredBody& b) {
|
|
97
|
+
if (!b.knownEmpty) b.knownEmpty = !b.body || b.body->IsEmpty();
|
|
98
|
+
return *b.knownEmpty;
|
|
99
|
+
}
|
|
100
|
+
inline manifold::Manifold::Error bodyStatus(ColoredBody& b) {
|
|
101
|
+
if (!b.knownStatus) b.knownStatus = b.body ? b.body->Status() : manifold::Manifold::Error::NoError;
|
|
102
|
+
return *b.knownStatus;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Emptiness of a union, from its operands' own flags and nothing else:
|
|
106
|
+
// non-empty if any operand is known non-empty, empty if every one is known
|
|
107
|
+
// empty, otherwise unknown (asked of Manifold later, if anyone ever asks).
|
|
108
|
+
// `get` maps a range element to its ColoredBody.
|
|
109
|
+
template <typename Range, typename Get>
|
|
110
|
+
std::optional<bool> unionEmptyOf(const Range& ops, Get get) {
|
|
111
|
+
bool allKnownEmpty = true;
|
|
112
|
+
for (const auto& o : ops) {
|
|
113
|
+
const ColoredBody& b = get(o);
|
|
114
|
+
if (b.knownEmpty && !*b.knownEmpty) return false;
|
|
115
|
+
if (!b.knownEmpty) allKnownEmpty = false;
|
|
116
|
+
}
|
|
117
|
+
if (allKnownEmpty) return true;
|
|
118
|
+
return std::nullopt;
|
|
119
|
+
}
|
|
120
|
+
|
|
78
121
|
// `EvalContext::color`/CSGParams round-trip: a resolve function reads
|
|
79
122
|
// ctx.color (double-precision, matching Value's own numeric type) but a
|
|
80
123
|
// generate function only sees CSGParams (Value-typed, no ctx access) -- so
|
|
@@ -15,9 +15,21 @@ namespace oscadeval {
|
|
|
15
15
|
// must keep this struct alive for as long as evaluation runs against
|
|
16
16
|
// .rootScope/.processedNodes, since declarations injected from a used file
|
|
17
17
|
// are raw (non-owning) pointers into it.
|
|
18
|
+
// One used file's own top-level assignments, in source order, keyed by
|
|
19
|
+
// that file's root Scope (the one its injected declarations were
|
|
20
|
+
// re-anchored onto). Evaluator::setUsedFileGlobals takes the list so a
|
|
21
|
+
// used file's globals can be evaluated ONCE per run, eagerly, the first
|
|
22
|
+
// time anything in that file reads one -- see Evaluator::fileGlobal.
|
|
23
|
+
// Raw pointers into usedFileAsts, same lifetime contract as processedNodes.
|
|
24
|
+
struct UsedFileGlobals {
|
|
25
|
+
const oscad::Scope* root = nullptr;
|
|
26
|
+
std::vector<const oscad::ASTNode*> assignments;
|
|
27
|
+
};
|
|
28
|
+
|
|
18
29
|
struct ResolvedUseScopes {
|
|
19
30
|
std::vector<std::vector<std::unique_ptr<oscad::ASTNode>>> usedFileAsts;
|
|
20
31
|
std::vector<std::unique_ptr<oscad::Scope>> usedFileScopes;
|
|
32
|
+
std::vector<UsedFileGlobals> usedFileGlobals;
|
|
21
33
|
// What currentFile should actually be evaluated as: declarations
|
|
22
34
|
// injected via `use` (raw pointers into usedFileAsts, above), followed
|
|
23
35
|
// by currentFile's own nodes (raw pointers into the caller-owned
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "openscad_cpp_evaluator/csg_node.hpp"
|
|
8
8
|
#include "openscad_cpp_evaluator/debug_hooks.hpp"
|
|
9
9
|
#include "openscad_cpp_evaluator/eval_context.hpp"
|
|
10
|
+
#include "openscad_cpp_evaluator/eval_use.hpp"
|
|
10
11
|
#include "openscad_cpp_evaluator/eval_error.hpp"
|
|
11
12
|
#include "openscad_cpp_evaluator/font_provider.hpp"
|
|
12
13
|
#include "openscad_cpp_evaluator/manifold_cache.hpp"
|
|
@@ -285,6 +286,11 @@ public:
|
|
|
285
286
|
const oscad::ASTNode* producer);
|
|
286
287
|
|
|
287
288
|
std::unordered_map<uint32_t, const oscad::ASTNode*> idToNode;
|
|
289
|
+
// Nodes whose bodies were actually generated this run (not served from
|
|
290
|
+
// the ManifoldCache). Reset per run; a diagnostic, and the only honest
|
|
291
|
+
// way for a test to tell a cache hit from a miss now that an inner
|
|
292
|
+
// node's bodies are released the moment its parent has consumed them.
|
|
293
|
+
std::size_t generatedNodeCount = 0;
|
|
288
294
|
std::unordered_map<uint32_t, std::optional<std::array<float, 4>>> idToColor;
|
|
289
295
|
|
|
290
296
|
// Called by primitive-construction generate functions (cube, sphere,
|
|
@@ -1396,6 +1402,11 @@ public:
|
|
|
1396
1402
|
// the same decision evalUserFunction*/evalFunctionLiteral* already
|
|
1397
1403
|
// make before calling lookupOrCompileChunk/lookupCompiledLiteralChunk
|
|
1398
1404
|
// themselves.
|
|
1405
|
+
// Every used file's own top-level assignments (ResolvedUseScopes::
|
|
1406
|
+
// usedFileGlobals), so fileGlobal() can evaluate a file's globals once.
|
|
1407
|
+
// A caller that skips this keeps the old per-read fallback for used files.
|
|
1408
|
+
void setUsedFileGlobals(std::vector<UsedFileGlobals> globals) { usedFileGlobals_ = std::move(globals); }
|
|
1409
|
+
|
|
1399
1410
|
bool useBytecodeVm() const {
|
|
1400
1411
|
return bytecodeVmEnabled() && (!debugHooks_.debugHook || fastContinueBreakpoints_.has_value());
|
|
1401
1412
|
}
|
|
@@ -1941,6 +1952,25 @@ private:
|
|
|
1941
1952
|
// lifetime contract as every other raw AST/EvalContext pointer this
|
|
1942
1953
|
// class holds).
|
|
1943
1954
|
const EvalContext* rootCtx_ = nullptr;
|
|
1955
|
+
|
|
1956
|
+
// File-level globals are evaluated ONCE per run. The main file's (and
|
|
1957
|
+
// every include's) already are, by the top-level statement pass, into
|
|
1958
|
+
// rootCtx_'s own let_ level. A used file's are not -- `use` injects only
|
|
1959
|
+
// its declarations -- so the first read of any global from that file
|
|
1960
|
+
// evaluates ALL of its assignments, in source order, into a context of
|
|
1961
|
+
// its own, kept here for the rest of the run. Before this, a function
|
|
1962
|
+
// reading a global re-evaluated the assignment's expression on every
|
|
1963
|
+
// read (evalIdentifier's scope fallback): `r = rands(0,1,1); function
|
|
1964
|
+
// g() = r;` gave `g() != g()`, and a 20K-element global list read 2000
|
|
1965
|
+
// times from a function cost 9.7s. OpenSCAD itself re-evaluates a used
|
|
1966
|
+
// file's globals per CALL, which is a long-standing upstream bug, not
|
|
1967
|
+
// behaviour to match. Returns nullopt for a file this evaluator was
|
|
1968
|
+
// never told about (setUsedFileGlobals), so such a caller keeps the
|
|
1969
|
+
// old fallback; nullptr for a known file whose global is not (yet)
|
|
1970
|
+
// assigned -- a forward read, undef in OpenSCAD too.
|
|
1971
|
+
std::optional<const Value*> fileGlobal(const oscad::Scope& fileRoot, const std::string& name);
|
|
1972
|
+
std::vector<UsedFileGlobals> usedFileGlobals_;
|
|
1973
|
+
std::unordered_map<const oscad::Scope*, EvalContext> usedFileCtx_;
|
|
1944
1974
|
// The most recent ctx seen by evalStatement() -- error()'s only way to
|
|
1945
1975
|
// reach a locals snapshot for its own errorBreak hook call, since
|
|
1946
1976
|
// error() itself (called from dozens of builtin resolve functions)
|
|
@@ -53,7 +53,8 @@ struct ExportObject {
|
|
|
53
53
|
// 1-based index is appended to `openParts` for the caller to warn about.
|
|
54
54
|
std::vector<ExportObject> splitBodiesForExport(const std::vector<ColoredBody>& bodies,
|
|
55
55
|
std::vector<int>* openParts = nullptr,
|
|
56
|
-
bool splitComponents = false
|
|
56
|
+
bool splitComponents = false,
|
|
57
|
+
bool splitColors = true);
|
|
57
58
|
|
|
58
59
|
// Writes a binary STL: composes every body's mesh into one solid (bodies
|
|
59
60
|
// with no `.body` -- 2D-only sections -- or an empty Manifold are
|
|
@@ -236,6 +237,12 @@ struct ExportOptions {
|
|
|
236
237
|
// pieces it is in; see that function's own comment for why the default
|
|
237
238
|
// moved here.
|
|
238
239
|
bool splitComponents = false;
|
|
240
|
+
// Rule 2 of splitBodiesForExport: one object per colour. That is what
|
|
241
|
+
// a MULTI-MATERIAL print wants -- each colour becomes something the
|
|
242
|
+
// slicer assigns to a filament. Turn it off for a single-material
|
|
243
|
+
// print, which wants one welded solid instead: no seams between
|
|
244
|
+
// colour regions, and no per-triangle colour it has no use for.
|
|
245
|
+
bool splitColors = true;
|
|
239
246
|
// .svg only.
|
|
240
247
|
ExportSvgOptions svg;
|
|
241
248
|
// .pdf only.
|
|
@@ -186,7 +186,7 @@ public:
|
|
|
186
186
|
}
|
|
187
187
|
dirty_.erase(it);
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
markDead(level);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// nullptr if `name` has no binding on `myLevel`'s own ancestry chain.
|
|
@@ -211,7 +211,7 @@ public:
|
|
|
211
211
|
const std::vector<Entry>& vec = it->second;
|
|
212
212
|
auto idx = static_cast<long>(vec.size()) - 1;
|
|
213
213
|
int ancestor = myLevel;
|
|
214
|
-
while (ancestor
|
|
214
|
+
while (ancestor > 0 && idx >= 0) {
|
|
215
215
|
const int entryLevel = vec[static_cast<size_t>(idx)].level;
|
|
216
216
|
if (entryLevel > ancestor) {
|
|
217
217
|
--idx;
|
|
@@ -236,6 +236,11 @@ public:
|
|
|
236
236
|
auto it = stacks_.find(name);
|
|
237
237
|
return it == stacks_.end() ? 0 : it->second.size();
|
|
238
238
|
}
|
|
239
|
+
// Test-only: how many level slots `parent_` currently holds (index 0
|
|
240
|
+
// included) -- lets ScopeTrailStorageTest assert that popping levels in
|
|
241
|
+
// call/return order really does shrink it back, and that an escaping
|
|
242
|
+
// closure's still-live level stops the shrink at itself.
|
|
243
|
+
size_t debugLevelSlotsForTesting() const { return parent_.size(); }
|
|
239
244
|
|
|
240
245
|
// Ancestry-filtered snapshot -- O(total distinct names ever bound so
|
|
241
246
|
// far in the whole evaluation), not O(names visible now). Only used
|
|
@@ -272,6 +277,27 @@ private:
|
|
|
272
277
|
std::vector<int> parent_{0};
|
|
273
278
|
int nextLevel_ = 0;
|
|
274
279
|
|
|
280
|
+
// A popped level is marked -1 (0 is a live level with no parent -- an
|
|
281
|
+
// isolated root -- so the two must stay distinguishable). Then, whenever
|
|
282
|
+
// the level being popped is the newest one, `parent_` shrinks back past
|
|
283
|
+
// every dead level above the newest still-live one, and its number is
|
|
284
|
+
// handed out again by the next openLevel(). This keeps every invariant
|
|
285
|
+
// lookup() relies on: a reused number is greater than every level still
|
|
286
|
+
// holding an entry (all higher levels were dead, and popLevel already
|
|
287
|
+
// removed their entries), so a name's push-stack stays ascending, and a
|
|
288
|
+
// live level's number never changes -- an escaping closure that keeps a
|
|
289
|
+
// level alive simply stops the shrink at it. Without this, `parent_`
|
|
290
|
+
// grew by one int per level for the whole run and never shrank: 3 x
|
|
291
|
+
// 64MB for a 13M-call script, plus the copy of each doubling.
|
|
292
|
+
void markDead(int level) {
|
|
293
|
+
parent_[static_cast<size_t>(level)] = -1;
|
|
294
|
+
if (level == nextLevel_) {
|
|
295
|
+
while (nextLevel_ > 0 && parent_[static_cast<size_t>(nextLevel_)] < 0) {
|
|
296
|
+
parent_.pop_back();
|
|
297
|
+
--nextLevel_;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
275
301
|
};
|
|
276
302
|
|
|
277
303
|
// Per-EvalContext handle onto a shared ScopeTrailStorage<T>: this view's
|
|
@@ -473,7 +499,7 @@ public:
|
|
|
473
499
|
}
|
|
474
500
|
dirty_.erase(it);
|
|
475
501
|
}
|
|
476
|
-
|
|
502
|
+
markDead(level);
|
|
477
503
|
}
|
|
478
504
|
|
|
479
505
|
// Same sorted-merge ancestry walk as ScopeTrailStorage::lookup -- see
|
|
@@ -485,7 +511,7 @@ public:
|
|
|
485
511
|
const std::vector<Entry>& vec = stacks_[static_cast<size_t>(id)];
|
|
486
512
|
auto idx = static_cast<long>(vec.size()) - 1;
|
|
487
513
|
int ancestor = myLevel;
|
|
488
|
-
while (ancestor
|
|
514
|
+
while (ancestor > 0 && idx >= 0) {
|
|
489
515
|
const int entryLevel = vec[static_cast<size_t>(idx)].level;
|
|
490
516
|
if (entryLevel > ancestor) {
|
|
491
517
|
--idx;
|
|
@@ -505,6 +531,7 @@ public:
|
|
|
505
531
|
int id = intern_->idFor(name);
|
|
506
532
|
return id >= 0 && static_cast<size_t>(id) < stacks_.size() ? stacks_[static_cast<size_t>(id)].size() : 0;
|
|
507
533
|
}
|
|
534
|
+
size_t debugLevelSlotsForTesting() const { return parent_.size(); } // see ScopeTrailStorage's
|
|
508
535
|
|
|
509
536
|
std::vector<std::pair<std::string, T>> items(int myLevel) const {
|
|
510
537
|
std::vector<std::pair<std::string, T>> result;
|
|
@@ -530,6 +557,15 @@ private:
|
|
|
530
557
|
std::vector<int> parent_{0}; // see ScopeTrailStorage::parent_
|
|
531
558
|
int nextLevel_ = 0;
|
|
532
559
|
|
|
560
|
+
void markDead(int level) { // see ScopeTrailStorage::markDead
|
|
561
|
+
parent_[static_cast<size_t>(level)] = -1;
|
|
562
|
+
if (level == nextLevel_) {
|
|
563
|
+
while (nextLevel_ > 0 && parent_[static_cast<size_t>(nextLevel_)] < 0) {
|
|
564
|
+
parent_.pop_back();
|
|
565
|
+
--nextLevel_;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
533
569
|
};
|
|
534
570
|
|
|
535
571
|
// Per-EvalContext handle onto a shared IndexedScopeTrailStorage<T> -- the
|
|
@@ -123,8 +123,11 @@ class RootScope:
|
|
|
123
123
|
|
|
124
124
|
class _CSGNode:
|
|
125
125
|
"""One resolved (and, after evaluate(), generated) node in the CSG tree --
|
|
126
|
-
mirrors the reference's CSGNode dataclass. `bodies` is
|
|
127
|
-
|
|
126
|
+
mirrors the reference's CSGNode dataclass. `bodies` is always empty since
|
|
127
|
+
v1.17.0: an inner node's bodies are released as soon as its parent has
|
|
128
|
+
consumed them, and converting every node's mesh to numpy on every
|
|
129
|
+
evaluate() was work nothing read (format_csg_tree() prints structure,
|
|
130
|
+
not geometry; the rendered bodies come back separately). `params` is
|
|
128
131
|
plain Python data (numbers/strings/bools/lists/dicts/OscObject), never an
|
|
129
132
|
AST pointer -- eagerly converted, no lifetime tie to the Evaluator that
|
|
130
133
|
produced it. Used by format_csg_tree(); not otherwise part of the public
|
|
@@ -293,6 +296,7 @@ def strip_slivers(verts, tris):
|
|
|
293
296
|
|
|
294
297
|
def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
|
|
295
298
|
strip_slivers: bool = True, split_components: bool = False,
|
|
299
|
+
split_colors: bool = True,
|
|
296
300
|
svg_fill: bool = False, svg_fill_color: str = "white",
|
|
297
301
|
svg_stroke: bool = True, svg_stroke_color: str = "black",
|
|
298
302
|
svg_stroke_width: float = 0.35, pdf_options: dict | None = None) -> list:
|
|
@@ -316,6 +320,12 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
|
|
|
316
320
|
`split_components` gives every disconnected piece its own object in the
|
|
317
321
|
multi-object formats. Off by default, which is what OpenSCAD writes.
|
|
318
322
|
|
|
323
|
+
`split_colors` (on by default) writes one object per colour -- what a
|
|
324
|
+
MULTI-MATERIAL print wants, each colour being something the slicer
|
|
325
|
+
assigns to a filament. Turn it off for a single-material print:
|
|
326
|
+
everything welds into one solid, with no seams between colour regions
|
|
327
|
+
and no per-triangle colour it has no use for.
|
|
328
|
+
|
|
319
329
|
The `svg_*` arguments are OpenSCAD's `-O export-svg/...` set and apply
|
|
320
330
|
to `.svg` only, which writes a 2D model at 1:1 in millimetres.
|
|
321
331
|
`svg_stroke`/`svg_stroke_width` also pad the page, so they are not
|
|
@@ -335,7 +345,7 @@ def export_model(path: str, geometry, format: str = "", ascii_stl: bool = False,
|
|
|
335
345
|
"""
|
|
336
346
|
try:
|
|
337
347
|
return list(_ext.export_model(path, geometry, format, ascii_stl, strip_slivers,
|
|
338
|
-
split_components, svg_fill, svg_fill_color,
|
|
348
|
+
split_components, split_colors, svg_fill, svg_fill_color,
|
|
339
349
|
svg_stroke, svg_stroke_color, svg_stroke_width,
|
|
340
350
|
pdf_options))
|
|
341
351
|
except Exception as e:
|
|
@@ -286,6 +286,14 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
|
|
|
286
286
|
std::vector<Part2d> res2d;
|
|
287
287
|
bool have2d = false;
|
|
288
288
|
size_t idx = 0;
|
|
289
|
+
// Whether the 3D operands could possibly disagree on colour. Only then
|
|
290
|
+
// is attachTriColors worth its GetMeshGL(), which evaluates the boolean
|
|
291
|
+
// and copies the whole mesh out -- at EVERY level of a nested union
|
|
292
|
+
// chain, when nothing but the top ever needed it. Operands that all
|
|
293
|
+
// carry the same colour and no per-triangle colours of their own can
|
|
294
|
+
// only produce a uniformly coloured result.
|
|
295
|
+
std::optional<std::optional<std::array<float, 4>>> firstColor;
|
|
296
|
+
bool mixedColors = false;
|
|
289
297
|
|
|
290
298
|
size_t stmtIndex = 0;
|
|
291
299
|
for (const Value& sizeVal : groupSizes) {
|
|
@@ -314,7 +322,7 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
|
|
|
314
322
|
allDo.insert(allDo.end(), split.displayOnly.begin(), split.displayOnly.end());
|
|
315
323
|
|
|
316
324
|
std::vector<ColoredBody> bodies3d, sections2d;
|
|
317
|
-
for (
|
|
325
|
+
for (ColoredBody& c : split.foreground) {
|
|
318
326
|
// A body whose own Manifold::Status() isn't NoError (e.g.
|
|
319
327
|
// NonFiniteVertex, from a degenerate accumulated transform deep
|
|
320
328
|
// in an unrelated ancestor's positioning math -- found via a
|
|
@@ -331,7 +339,12 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
|
|
|
331
339
|
// operand and unioning everything else is the closest match to
|
|
332
340
|
// its own behavior available without new numerical-robustness
|
|
333
341
|
// work on Manifold's own boolean ops.
|
|
334
|
-
if (c.body && c
|
|
342
|
+
if (c.body && bodyStatus(c) == manifold::Manifold::Error::NoError) bodies3d.push_back(c);
|
|
343
|
+
}
|
|
344
|
+
for (const ColoredBody& c : bodies3d) {
|
|
345
|
+
if (c.triColors) mixedColors = true;
|
|
346
|
+
if (!firstColor) firstColor = c.color;
|
|
347
|
+
else if (*firstColor != c.color) mixedColors = true;
|
|
335
348
|
}
|
|
336
349
|
for (const ColoredBody& c : split.foreground) {
|
|
337
350
|
if (c.section) sections2d.push_back(c);
|
|
@@ -358,17 +371,33 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
|
|
|
358
371
|
if (!bodies3d.empty()) {
|
|
359
372
|
manifold::Manifold grp = *bodies3d.front().body;
|
|
360
373
|
for (size_t i = 1; i < bodies3d.size(); ++i) grp = grp + *bodies3d[i].body;
|
|
374
|
+
// The result stays LAZY inside Manifold: its emptiness is derived
|
|
375
|
+
// from the operands' flags here rather than asked, so a chain of
|
|
376
|
+
// unions under transforms is evaluated once, at the top, instead
|
|
377
|
+
// of once per level (see ColoredBody::knownEmpty).
|
|
378
|
+
const std::optional<bool> grpEmpty =
|
|
379
|
+
unionEmptyOf(bodies3d, [](const ColoredBody& b) -> const ColoredBody& { return b; });
|
|
380
|
+
const auto known = [](const std::optional<bool>& e, bool v) { return e && *e == v; };
|
|
361
381
|
if (!res3d) {
|
|
362
382
|
ColoredBody cb;
|
|
363
383
|
cb.body = std::move(grp);
|
|
364
384
|
cb.color = bodies3d.front().color;
|
|
385
|
+
cb.knownStatus = manifold::Manifold::Error::NoError; // every operand was
|
|
386
|
+
cb.knownEmpty = grpEmpty;
|
|
365
387
|
res3d = std::move(cb);
|
|
366
388
|
} else if (op == "union") {
|
|
367
389
|
res3d->body = *res3d->body + grp;
|
|
390
|
+
if (known(res3d->knownEmpty, false) || known(grpEmpty, false)) res3d->knownEmpty = false;
|
|
391
|
+
else if (known(res3d->knownEmpty, true) && known(grpEmpty, true)) res3d->knownEmpty = true;
|
|
392
|
+
else res3d->knownEmpty.reset();
|
|
368
393
|
} else if (op == "difference") {
|
|
369
394
|
res3d->body = *res3d->body - grp;
|
|
395
|
+
if (known(res3d->knownEmpty, true)) res3d->knownEmpty = true;
|
|
396
|
+
else if (!known(grpEmpty, true)) res3d->knownEmpty.reset(); // subtracting nothing changes nothing
|
|
370
397
|
} else if (op == "intersection") {
|
|
371
398
|
res3d->body = *res3d->body ^ grp;
|
|
399
|
+
if (known(res3d->knownEmpty, true) || known(grpEmpty, true)) res3d->knownEmpty = true;
|
|
400
|
+
else res3d->knownEmpty.reset();
|
|
372
401
|
}
|
|
373
402
|
}
|
|
374
403
|
if (!sections2d.empty()) {
|
|
@@ -394,7 +423,7 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
|
|
|
394
423
|
}
|
|
395
424
|
}
|
|
396
425
|
|
|
397
|
-
if (res3d && res3d->body) attachTriColors(ev, *res3d);
|
|
426
|
+
if (res3d && res3d->body && mixedColors) attachTriColors(ev, *res3d);
|
|
398
427
|
|
|
399
428
|
std::vector<ColoredBody> result;
|
|
400
429
|
if (res3d) result.push_back(std::move(*res3d));
|
|
@@ -65,8 +65,8 @@ namespace {
|
|
|
65
65
|
// `union() { color("red") stroke(...); text(); }` had no red recorded for
|
|
66
66
|
// the stroke's runs at all, so every run looked identical, the merge kept
|
|
67
67
|
// the first child's colour, and the whole thing came out red.
|
|
68
|
-
void recordRunColors(Evaluator& ev,
|
|
69
|
-
if (!b.body || b
|
|
68
|
+
void recordRunColors(Evaluator& ev, ColoredBody& b, const std::optional<std::array<float, 4>>& rgba) {
|
|
69
|
+
if (!b.body || bodyIsEmpty(b)) return;
|
|
70
70
|
// A body that is still one original knows its own ID without building
|
|
71
71
|
// a mesh -- the common case, and the cheap one.
|
|
72
72
|
const int original = b.body->OriginalID();
|
|
@@ -68,6 +68,7 @@ ColoredBody combineBodies(const std::vector<ColoredBody>& bodies) {
|
|
|
68
68
|
ColoredBody cb;
|
|
69
69
|
cb.body = std::move(merged);
|
|
70
70
|
cb.color = bodies3d.front()->color;
|
|
71
|
+
cb.knownEmpty = unionEmptyOf(bodies3d, [](const ColoredBody* b) -> const ColoredBody& { return *b; });
|
|
71
72
|
return cb;
|
|
72
73
|
}
|
|
73
74
|
std::vector<const ColoredBody*> sections;
|
|
@@ -34,7 +34,18 @@ CSGParams resolveText(Evaluator& ev, const oscad::ModularCall& node, EvalContext
|
|
|
34
34
|
auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
|
|
35
35
|
const std::string text = asStringOr(getArg(args, 0, "text", Value{std::string("")}), "");
|
|
36
36
|
const double size = toDoubleLenient(getArg(args, 1, "size", Value{10.0}));
|
|
37
|
-
|
|
37
|
+
// font is POSITIONAL, at index 2: `text("Hi", 10, "Liberation Sans")`
|
|
38
|
+
// is how people write it and how the reference reads it -- measured
|
|
39
|
+
// against the 2026.02.01 binary, whose bounding box for the
|
|
40
|
+
// positional form matches the named one exactly.
|
|
41
|
+
//
|
|
42
|
+
// Only font. halign/valign/spacing/direction/language/script stay
|
|
43
|
+
// name-only, because the same measurement shows the reference
|
|
44
|
+
// ignoring them positionally: `text("Hi", 10, "F", "center")` is NOT
|
|
45
|
+
// centred there, while halign="center" is. text() and textmetrics()
|
|
46
|
+
// genuinely differ here -- textmetrics takes all nine positionally --
|
|
47
|
+
// so neither can be assumed from the other.
|
|
48
|
+
const std::string fontSpec = asStringOr(getArg(args, 2, "font", Value{std::string("")}), "");
|
|
38
49
|
const std::string halign = asStringOr(getArg(args, std::nullopt, "halign", Value{std::string("left")}), "left");
|
|
39
50
|
const std::string valign = asStringOr(getArg(args, std::nullopt, "valign", Value{std::string("baseline")}), "baseline");
|
|
40
51
|
const double spacing = toDoubleLenient(getArg(args, std::nullopt, "spacing", Value{1.0}));
|