openscad-cpp-evaluator 0.49.0__tar.gz → 0.50.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/CLAUDE.md +45 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/PKG-INFO +1 -1
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/pyproject.toml +1 -1
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/builtins.hpp +5 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/function_builtins.cpp +14 -3
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/registry.cpp +3 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/topology.cpp +255 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_booleans.cpp +227 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_feature_detection.cpp +1 -1
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_function_builtins.cpp +14 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.github/workflows/ci.yml +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.github/workflows/wheels.yml +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.gitignore +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/.gitmodules +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/LICENSE +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/README.md +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/bindings/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/bindings/ast_to_py.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/bindings/module.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/cmake/embed_font.cmake +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/eval_perf_benchmark.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/manifold_cache_reuse.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/examples/minimal_debugger.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/.gitignore +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/CLAUDE.md +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/README.md +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/api.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/comments.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/position.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/scope.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/export.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/profile.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/segments.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/value.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/python/openscad_cpp_evaluator/__init__.py +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/resources/fonts/LICENSE +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/resources/fonts/LiberationSans-Regular.ttf +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/booleans.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/call_args.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/color.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/control.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/dxf_dim.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/extrude.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/import.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/modifiers.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/primitives_2d.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/primitives_3d.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/roof.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/segments.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/surface.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/text.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/transforms.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/bytecode_compiler.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/bytecode_vm.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/colored_body.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/csg_generate.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/csg_resolve.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/css_colors.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/debug/debug_repl.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/debug_profile.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/eval_context.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/eval_error.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/eval_use.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/export.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/expr_eval.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/dxf_import.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/mesh_import.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/surface_load.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/import/svg_import.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/manifold_cache.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/measure_geometry.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/mesh_check.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/stmt_eval.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/text/font_match.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/text/freetype_font_provider.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/text/text_metrics.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/user_calls.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/value.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/zip_stored.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_bytecode_compiler.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_children_separate.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_cli.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_control_flow.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_csg_tree.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_debug_hooks.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_dxf_svg_import.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_eval_context.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_export.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_expr_eval.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_extrude_roof.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_helpers.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_import_export.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_manifold_cache.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_mesh_check.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_multi_color_merge.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_primitives.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_profiling.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_python_bindings.py +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_render_expr.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_scope_trail.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_scoping.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_surface.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_tail_calls.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_text.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_transforms.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_unexpected_args.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_use.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_value.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_viewport_params.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_zip_stored.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/CMakeLists.txt +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/cli_lib.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/cli_lib.hpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tools/cli/main.cpp +0 -0
- {openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/uv.lock +0 -0
|
@@ -460,6 +460,51 @@ grep for `ponytail:`.
|
|
|
460
460
|
unknown arguments -- `children(separate=true)` renders the wrong shape there with no warning --
|
|
461
461
|
and both read as `undef` in OpenSCAD, so a guard written against them works in both.
|
|
462
462
|
|
|
463
|
+
- **`levelset(field, bounds, isovalue)`** (`src/builtins/topology.cpp`, feature name `levelset`) —
|
|
464
|
+
a solid from a grid of sampled values: `field[i][j][k]` with i,j,k mapping to x,y,z. Wraps
|
|
465
|
+
`Manifold::LevelSet` with a C++ trilinear-sampling lambda; no new Manifold API.
|
|
466
|
+
**The field is a grid OR a `function(x,y,z)`, and the two trade off against each other** —
|
|
467
|
+
neither is simply better:
|
|
468
|
+
|
|
469
|
+
| | sampling | parallel | accuracy | ceiling |
|
|
470
|
+
|---|---|---|---|---|
|
|
471
|
+
| grid `field[i][j][k]` | 0.41 µs | **yes** | capped by the grid | ~550 MB at 200³ |
|
|
472
|
+
| `function(x,y,z)` | 0.96 µs | **no** | Manifold snaps toward the true surface | none |
|
|
473
|
+
|
|
474
|
+
Measured at matched resolution against the analytic sphere: **50³ grid 0.23 s (−0.244%) vs
|
|
475
|
+
function 0.32 s (−0.103%); 100³ grid 1.06 s (−0.059%) vs function 1.91 s (−0.025%)**. So the
|
|
476
|
+
function form is ~1.5× slower and ~2.4× more accurate. Both still beat BOSL2.
|
|
477
|
+
`canParallel` is the crux and is **not a tuning knob**: true for a grid because the lambda is
|
|
478
|
+
pure C++, false for a function because Manifold warns that parallel policies "will crash
|
|
479
|
+
language runtimes with runtime locks that expect to not be called back by unregistered threads".
|
|
480
|
+
The function form also needs `edge=` explicitly — there is no grid to infer spacing from and the
|
|
481
|
+
cost is cubic in it, so guessing would silently produce either a useless mesh or a ten-minute
|
|
482
|
+
one.
|
|
483
|
+
Measured against BOSL2's `isosurface.scad` (217 KB of marching cubes in script), same field:
|
|
484
|
+
**50³ 0.26 s vs 2.07 s (8.0×), 100³ 1.15 s vs 14.59 s (12.7×)**.
|
|
485
|
+
The trade is **memory**: ~67 bytes/cell, so 200³ ≈ 550 MB is the practical ceiling. A callback
|
|
486
|
+
samples lazily and has none.
|
|
487
|
+
**Accuracy is bounded by the grid, not by Manifold.** It samples on a body-centred cubic lattice
|
|
488
|
+
(`sdf.cpp:440-446`) so a plain cubic grid does not line up and the lambda interpolates — same
|
|
489
|
+
accuracy as grid-based marching cubes in script, worse than `LevelSet` given a true SDF. Hence
|
|
490
|
+
`tolerance = -1` is forced: a positive value makes Manifold do extra evaluations per vertex that
|
|
491
|
+
would only re-interpolate data already used. `edgeLength` defaults to the grid spacing for the
|
|
492
|
+
same reason.
|
|
493
|
+
Calling the field function from the generate pass has no live `EvalContext`, so a root is built
|
|
494
|
+
from the closure's own scope — `$`-variables sit at their defaults inside it.
|
|
495
|
+
**An explicitly-undef optional argument counts as ABSENT**, not as a bad one — the same rule in
|
|
496
|
+
`linear_solve` (`b`) and `levelset` (`isovalue`). BOSL2 reaches a builtin it also defines by
|
|
497
|
+
wrapping it in `builtins.scad`, which `use <>`s nothing, so the names there bind to the builtins:
|
|
498
|
+
`function _linear_solve(A, b) = linear_solve(A, b);`. That wrapper has a fixed signature and so
|
|
499
|
+
forwards `b` even when the caller passed none, so treating undef as a *bad* argument warned on
|
|
500
|
+
every `determinant()`-style call. Passing undef is idiomatically the same as not passing in
|
|
501
|
+
OpenSCAD, which is how BOSL2 threads optional arguments through wrappers throughout.
|
|
502
|
+
Verified against OpenSCAD 2026.02.01: the wrapper pattern resolves identically there.
|
|
503
|
+
Sign convention: Manifold takes positive as inside; a distance field is the other way round, so
|
|
504
|
+
the default flips it and `invert=true` flips back.
|
|
505
|
+
Two behaviours that look alike and are opposite: an isovalue **nothing reaches** gives empty
|
|
506
|
+
geometry; one **everything satisfies** gives the whole bounding box. Both are tested.
|
|
507
|
+
|
|
463
508
|
- **`linear_solve(A, b)`** (`src/builtins/function_builtins.cpp`, feature name `linear-solve`) —
|
|
464
509
|
returns `.x` (solution), `.det`, `.singular`. Dispatches on shape:
|
|
465
510
|
|
|
@@ -183,6 +183,11 @@ std::vector<ColoredBody> generateHull(Evaluator& ev, const CSGParams& params,
|
|
|
183
183
|
Value builtinDxfDim(Evaluator& ev, const CallArgs& args, const oscad::ASTNode& node);
|
|
184
184
|
Value builtinDxfCross(Evaluator& ev, const CallArgs& args, const oscad::ASTNode& node);
|
|
185
185
|
|
|
186
|
+
CSGParams resolveLevelSet(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
|
|
187
|
+
std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params,
|
|
188
|
+
const std::vector<std::unique_ptr<CSGNode>>& children,
|
|
189
|
+
const oscad::ASTNode& node);
|
|
190
|
+
|
|
186
191
|
CSGParams resolveSimplify(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
|
|
187
192
|
std::vector<ColoredBody> generateSimplify(Evaluator& ev, const CSGParams& params,
|
|
188
193
|
const std::vector<std::unique_ptr<CSGNode>>& children,
|
{openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/src/builtins/function_builtins.cpp
RENAMED
|
@@ -913,6 +913,7 @@ const std::unordered_map<std::string, double>& featureLevels() {
|
|
|
913
913
|
static const std::unordered_map<std::string, double> levels = {
|
|
914
914
|
{"render-expr", 1.0}, // render() in expression position
|
|
915
915
|
{"linear-solve", 1.0}, // linear_solve(A, b) -> {x, det, singular}
|
|
916
|
+
{"levelset", 1.0}, // levelset(field, bounds, isovalue)
|
|
916
917
|
{"polyhedron-vnf", 1.0}, // polyhedron(vnf) / polyhedron(object)
|
|
917
918
|
{"separate-children", 1.0}, // children(..., separate=true)
|
|
918
919
|
{"minkowski-diff", 1.0}, // minkowski_difference()
|
|
@@ -1344,9 +1345,19 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
|
|
|
1344
1345
|
// The OpenSCAD release we track. version_num() is that same
|
|
1345
1346
|
// year/month/day folded as y * 10000 + m * 100 + d, exactly like the
|
|
1346
1347
|
// reference's own builtin_version_num (builtin_functions.cc).
|
|
1347
|
-
case BuiltinFnId::LinearSolve:
|
|
1348
|
-
|
|
1349
|
-
|
|
1348
|
+
case BuiltinFnId::LinearSolve: {
|
|
1349
|
+
// An explicitly-undef b counts as ABSENT, not as a bad argument.
|
|
1350
|
+
// A wrapper with a fixed signature -- BOSL2's builtins.scad
|
|
1351
|
+
// pattern, `function _linear_solve(A, b) = linear_solve(A, b);`
|
|
1352
|
+
// -- always forwards b, so determinant()-style callers that pass
|
|
1353
|
+
// no right-hand side would otherwise warn on every call.
|
|
1354
|
+
// Passing undef is idiomatically the same as not passing in
|
|
1355
|
+
// OpenSCAD, which is how optional arguments are threaded through
|
|
1356
|
+
// wrappers throughout BOSL2.
|
|
1357
|
+
const Value bArg = getArg(args, 1, "b", Value{});
|
|
1358
|
+
const bool haveB = !std::holds_alternative<std::monostate>(bArg);
|
|
1359
|
+
return builtinLinearSolve(ev, getArg(args, 0, "A", Value{}), bArg, haveB, node);
|
|
1360
|
+
}
|
|
1350
1361
|
case BuiltinFnId::SupportedFeature: {
|
|
1351
1362
|
const Value name = getArg(args, 0, "feature", Value{});
|
|
1352
1363
|
const std::string* s = std::get_if<std::string>(&name);
|
|
@@ -37,6 +37,7 @@ const std::unordered_map<std::string_view, ResolveFn>& resolveDispatch() {
|
|
|
37
37
|
{"fill", &resolveFill},
|
|
38
38
|
{"minkowski_difference", &resolveMinkowskiDifference},
|
|
39
39
|
{"simplify", &resolveSimplify},
|
|
40
|
+
{"levelset", &resolveLevelSet},
|
|
40
41
|
{"offset", &resolveOffset},
|
|
41
42
|
{"surface", &resolveSurface},
|
|
42
43
|
{"text", &resolveText},
|
|
@@ -78,6 +79,7 @@ const std::unordered_map<std::string_view, GenerateFn>& generateDispatch() {
|
|
|
78
79
|
{"fill", &generateFill},
|
|
79
80
|
{"minkowski_difference", &generateMinkowskiDifference},
|
|
80
81
|
{"simplify", &generateSimplify},
|
|
82
|
+
{"levelset", &generateLevelSet},
|
|
81
83
|
{"offset", &generateOffset},
|
|
82
84
|
{"surface", &generateSurface},
|
|
83
85
|
{"text", &generateText},
|
|
@@ -125,6 +127,7 @@ const std::vector<std::string>* builtinParamNames(const std::string& name) {
|
|
|
125
127
|
{"fill", {}},
|
|
126
128
|
{"minkowski_difference", {}},
|
|
127
129
|
{"simplify", {"tolerance"}},
|
|
130
|
+
{"levelset", {"field", "bounds", "isovalue", "invert", "edge"}},
|
|
128
131
|
{"offset", {"r", "delta", "chamfer"}},
|
|
129
132
|
{"surface", {"file", "center", "convexity", "invert"}},
|
|
130
133
|
{"text", {"text", "size", "font", "direction", "language", "script", "halign", "valign", "spacing"}},
|
|
@@ -451,4 +451,259 @@ std::vector<ColoredBody> generateSimplify(Evaluator& ev, const CSGParams& params
|
|
|
451
451
|
return out;
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
// -- levelset(field, bounds, isovalue) -------------------------------------
|
|
455
|
+
//
|
|
456
|
+
// A solid from a grid of sampled values. Wraps Manifold::LevelSet with a C++
|
|
457
|
+
// trilinear-sampling lambda over the array the script handed us.
|
|
458
|
+
//
|
|
459
|
+
// Why a grid rather than Manifold's own SDF-callback shape, which would be
|
|
460
|
+
// the obvious mapping: measured, building the field in OpenSCAD costs
|
|
461
|
+
// 0.41 us/sample against 0.96 us for a closure call per sample, because the
|
|
462
|
+
// arithmetic inlines into the list comprehension. More importantly a grid
|
|
463
|
+
// means the C++ side never re-enters the evaluator, so canParallel can be
|
|
464
|
+
// TRUE -- Manifold's docs warn that parallel policies "will crash language
|
|
465
|
+
// runtimes with runtime locks that expect to not be called back by
|
|
466
|
+
// unregistered threads", which is exactly what a callback design forfeits.
|
|
467
|
+
//
|
|
468
|
+
// The trade is memory: ~67 bytes per cell, so 200^3 is about 550 MB and that
|
|
469
|
+
// is the practical ceiling. A callback samples lazily and has no ceiling.
|
|
470
|
+
//
|
|
471
|
+
// Accuracy is bounded by the GRID, not by Manifold. Manifold samples on a
|
|
472
|
+
// body-centred cubic lattice (two interleaved cubic grids, sdf.cpp:440-446)
|
|
473
|
+
// and a plain cubic grid does not line up with those points, so the lambda
|
|
474
|
+
// interpolates. Same accuracy as a grid-based marching-cubes implementation
|
|
475
|
+
// in script; worse than LevelSet given a true SDF. Do not imply otherwise.
|
|
476
|
+
|
|
477
|
+
namespace {
|
|
478
|
+
|
|
479
|
+
struct ScalarField {
|
|
480
|
+
std::vector<double> v; // flat, x fastest
|
|
481
|
+
size_t nx = 0, ny = 0, nz = 0;
|
|
482
|
+
double at(size_t i, size_t j, size_t k) const { return v[(k * ny + j) * nx + i]; }
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// field[i][j][k] -> flat, checking it is a full rectangular block.
|
|
486
|
+
std::optional<ScalarField> readField(const Value& val) {
|
|
487
|
+
const ListPtr* xs = std::get_if<ListPtr>(&val);
|
|
488
|
+
if (!xs || !*xs || (*xs)->items.empty()) return std::nullopt;
|
|
489
|
+
ScalarField f;
|
|
490
|
+
f.nx = (*xs)->items.size();
|
|
491
|
+
for (size_t i = 0; i < f.nx; ++i) {
|
|
492
|
+
const ListPtr* ys = std::get_if<ListPtr>(&(*xs)->items[i]);
|
|
493
|
+
if (!ys || !*ys || (*ys)->items.empty()) return std::nullopt;
|
|
494
|
+
if (i == 0) f.ny = (*ys)->items.size();
|
|
495
|
+
else if ((*ys)->items.size() != f.ny) return std::nullopt;
|
|
496
|
+
for (size_t j = 0; j < f.ny; ++j) {
|
|
497
|
+
const ListPtr* zs = std::get_if<ListPtr>(&(*ys)->items[j]);
|
|
498
|
+
if (!zs || !*zs || (*zs)->items.empty()) return std::nullopt;
|
|
499
|
+
if (i == 0 && j == 0) {
|
|
500
|
+
f.nz = (*zs)->items.size();
|
|
501
|
+
f.v.assign(f.nx * f.ny * f.nz, 0.0);
|
|
502
|
+
} else if ((*zs)->items.size() != f.nz) {
|
|
503
|
+
return std::nullopt;
|
|
504
|
+
}
|
|
505
|
+
for (size_t k = 0; k < f.nz; ++k) {
|
|
506
|
+
const double* d = std::get_if<double>(&(*zs)->items[k]);
|
|
507
|
+
if (!d || !std::isfinite(*d)) return std::nullopt;
|
|
508
|
+
f.v[(k * f.ny + j) * f.nx + i] = *d;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return f;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
std::optional<std::array<double, 3>> readVec3(const Value& v) {
|
|
516
|
+
const ListPtr* l = std::get_if<ListPtr>(&v);
|
|
517
|
+
if (!l || !*l || (*l)->items.size() != 3) return std::nullopt;
|
|
518
|
+
std::array<double, 3> out{};
|
|
519
|
+
for (size_t i = 0; i < 3; ++i) {
|
|
520
|
+
const double* d = std::get_if<double>(&(*l)->items[i]);
|
|
521
|
+
if (!d || !std::isfinite(*d)) return std::nullopt;
|
|
522
|
+
out[i] = *d;
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
} // namespace
|
|
528
|
+
|
|
529
|
+
CSGParams resolveLevelSet(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx) {
|
|
530
|
+
auto [args, effCtx] = resolveCallArgs(ev, node.arguments, ctx);
|
|
531
|
+
CSGParams params;
|
|
532
|
+
params["field"] = getArg(args, 0, "field", Value{});
|
|
533
|
+
params["bounds"] = getArg(args, 1, "bounds", Value{});
|
|
534
|
+
params["isovalue"] = getArg(args, 2, "isovalue", Value{0.0});
|
|
535
|
+
params["invert"] = getArg(args, 3, "invert", Value{false});
|
|
536
|
+
params["edge"] = getArg(args, 4, "edge", Value{});
|
|
537
|
+
ev.evalChildren(node.children, effCtx);
|
|
538
|
+
return params;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params,
|
|
542
|
+
const std::vector<std::unique_ptr<CSGNode>>&,
|
|
543
|
+
const oscad::ASTNode& node) {
|
|
544
|
+
// Two shapes of field, and the choice matters beyond taste:
|
|
545
|
+
//
|
|
546
|
+
// a GRID -- faster to sample (0.41 vs 0.96 us) and meshes in
|
|
547
|
+
// PARALLEL, because nothing re-enters the evaluator.
|
|
548
|
+
// Accuracy is capped by the grid; memory is the ceiling.
|
|
549
|
+
// a FUNCTION -- Manifold picks its own sample points and can snap to
|
|
550
|
+
// the true surface, so it is MORE accurate, with no
|
|
551
|
+
// memory ceiling. But every sample is a closure call, and
|
|
552
|
+
// canParallel must be false or the evaluator gets called
|
|
553
|
+
// back from threads it knows nothing about.
|
|
554
|
+
const Value& fieldArg = params.at("field");
|
|
555
|
+
const ClosurePtr* fieldFn = std::get_if<ClosurePtr>(&fieldArg);
|
|
556
|
+
std::optional<ScalarField> field;
|
|
557
|
+
if (!fieldFn) {
|
|
558
|
+
field = readField(fieldArg);
|
|
559
|
+
if (!field) {
|
|
560
|
+
ev.warn("levelset(): field must be a function(x,y,z) or a rectangular field[i][j][k] of numbers",
|
|
561
|
+
&node.position());
|
|
562
|
+
return {};
|
|
563
|
+
}
|
|
564
|
+
if (field->nx < 2 || field->ny < 2 || field->nz < 2) {
|
|
565
|
+
ev.warn("levelset(): the field needs at least 2 samples along each axis", &node.position());
|
|
566
|
+
return {};
|
|
567
|
+
}
|
|
568
|
+
} else if (!*fieldFn || (*fieldFn)->node == nullptr || (*fieldFn)->node->parameters.size() < 3) {
|
|
569
|
+
ev.warn("levelset(): the field function needs three parameters, as in function(x,y,z) ...",
|
|
570
|
+
&node.position());
|
|
571
|
+
return {};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const ListPtr* bb = std::get_if<ListPtr>(¶ms.at("bounds"));
|
|
575
|
+
std::optional<std::array<double, 3>> lo, hi;
|
|
576
|
+
if (bb && *bb && (*bb)->items.size() == 2) {
|
|
577
|
+
lo = readVec3((*bb)->items[0]);
|
|
578
|
+
hi = readVec3((*bb)->items[1]);
|
|
579
|
+
}
|
|
580
|
+
if (!lo || !hi) {
|
|
581
|
+
ev.warn("levelset(): bounds must be [[x0,y0,z0],[x1,y1,z1]]", &node.position());
|
|
582
|
+
return {};
|
|
583
|
+
}
|
|
584
|
+
for (int a = 0; a < 3; ++a) {
|
|
585
|
+
if (!((*hi)[a] > (*lo)[a])) {
|
|
586
|
+
ev.warn("levelset(): bounds must be increasing along every axis", &node.position());
|
|
587
|
+
return {};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Same rule as linear_solve's b: an explicitly-undef argument is ABSENT,
|
|
592
|
+
// so a fixed-signature wrapper forwarding every parameter still works.
|
|
593
|
+
const Value& isoVal = params.at("isovalue");
|
|
594
|
+
double isovalue = 0.0;
|
|
595
|
+
if (!std::holds_alternative<std::monostate>(isoVal)) {
|
|
596
|
+
const double* isoArg = std::get_if<double>(&isoVal);
|
|
597
|
+
if (!isoArg) {
|
|
598
|
+
ev.warn("levelset(): isovalue must be a number", &node.position());
|
|
599
|
+
return {};
|
|
600
|
+
}
|
|
601
|
+
isovalue = *isoArg;
|
|
602
|
+
}
|
|
603
|
+
const bool invert = truthy(params.at("invert"));
|
|
604
|
+
|
|
605
|
+
const std::array<double, 3> origin = *lo;
|
|
606
|
+
std::array<double, 3> spacing{1.0, 1.0, 1.0};
|
|
607
|
+
double edge = 0.0;
|
|
608
|
+
if (field) {
|
|
609
|
+
// Grid spacing is corner-sample to corner-sample, so n-1 intervals.
|
|
610
|
+
spacing = {((*hi)[0] - (*lo)[0]) / static_cast<double>(field->nx - 1),
|
|
611
|
+
((*hi)[1] - (*lo)[1]) / static_cast<double>(field->ny - 1),
|
|
612
|
+
((*hi)[2] - (*lo)[2]) / static_cast<double>(field->nz - 1)};
|
|
613
|
+
// Finer than the grid buys nothing -- there is no information between
|
|
614
|
+
// samples -- and coarser throws away what the script paid to compute.
|
|
615
|
+
edge = std::min({spacing[0], spacing[1], spacing[2]});
|
|
616
|
+
}
|
|
617
|
+
if (const double* e = std::get_if<double>(¶ms.at("edge"))) {
|
|
618
|
+
if (*e > 0.0) edge = *e;
|
|
619
|
+
else ev.warn("levelset(): edge must be positive", &node.position());
|
|
620
|
+
}
|
|
621
|
+
if (edge <= 0.0) {
|
|
622
|
+
// No grid to infer it from. Guessing would silently pick either a
|
|
623
|
+
// useless mesh or a ten-minute one -- the cost is cubic in this
|
|
624
|
+
// number, so it is the caller's call to make.
|
|
625
|
+
ev.warn("levelset(): a function field needs edge= (the sample spacing)", &node.position());
|
|
626
|
+
return {};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// The function form: one closure call per sample. No live EvalContext at
|
|
630
|
+
// generate time, so a root is built from the closure's own scope -- the
|
|
631
|
+
// same approach the warp experiment used. $-variables are therefore at
|
|
632
|
+
// their defaults inside a field function.
|
|
633
|
+
std::optional<EvalContext> fnCtx;
|
|
634
|
+
std::array<std::string, 3> fnParams;
|
|
635
|
+
if (fieldFn) {
|
|
636
|
+
fnCtx = EvalContext::makeRoot((*fieldFn)->node->scope());
|
|
637
|
+
for (int a = 0; a < 3; ++a) fnParams[static_cast<size_t>(a)] = (*fieldFn)->node->parameters[static_cast<size_t>(a)]->name->name;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const auto sampleFn = [&](manifold::vec3 p) -> double {
|
|
641
|
+
BoundArgs bound;
|
|
642
|
+
const double xyz[3] = {p.x, p.y, p.z};
|
|
643
|
+
for (int a = 0; a < 3; ++a) bound.set(fnParams[static_cast<size_t>(a)], Value{xyz[a]});
|
|
644
|
+
const Value out = ev.evalFunctionLiteralFromBound(**fieldFn, std::move(bound), *fnCtx, &node.position());
|
|
645
|
+
const double* d = std::get_if<double>(&out);
|
|
646
|
+
// A field that returns junk at one point must not abort the whole
|
|
647
|
+
// build; treat it as far outside instead.
|
|
648
|
+
const double v = (d && std::isfinite(*d)) ? *d : std::numeric_limits<double>::max();
|
|
649
|
+
const double diff = v - isovalue;
|
|
650
|
+
return invert ? diff : -diff;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// Never dereferenced on the function path, but it must still be a valid
|
|
654
|
+
// reference -- binding one to a null pointer is undefined behaviour even
|
|
655
|
+
// where the lambda is never called.
|
|
656
|
+
const ScalarField emptyField;
|
|
657
|
+
const ScalarField& f = field ? *field : emptyField;
|
|
658
|
+
const auto sampleGrid = [&f, origin, spacing, isovalue, invert](manifold::vec3 p) -> double {
|
|
659
|
+
double g[3];
|
|
660
|
+
size_t i0[3];
|
|
661
|
+
double t[3];
|
|
662
|
+
const double pos[3] = {p.x, p.y, p.z};
|
|
663
|
+
const size_t n[3] = {f.nx, f.ny, f.nz};
|
|
664
|
+
for (int a = 0; a < 3; ++a) {
|
|
665
|
+
g[a] = (pos[a] - origin[a]) / spacing[a];
|
|
666
|
+
if (g[a] < 0.0) g[a] = 0.0;
|
|
667
|
+
const double maxg = static_cast<double>(n[a] - 1);
|
|
668
|
+
if (g[a] > maxg) g[a] = maxg;
|
|
669
|
+
i0[a] = static_cast<size_t>(g[a]);
|
|
670
|
+
if (i0[a] > n[a] - 2) i0[a] = n[a] - 2;
|
|
671
|
+
t[a] = g[a] - static_cast<double>(i0[a]);
|
|
672
|
+
}
|
|
673
|
+
double acc = 0.0;
|
|
674
|
+
for (int c = 0; c < 8; ++c) {
|
|
675
|
+
const size_t di = static_cast<size_t>(c & 1), dj = static_cast<size_t>((c >> 1) & 1),
|
|
676
|
+
dk = static_cast<size_t>((c >> 2) & 1);
|
|
677
|
+
const double w = (di ? t[0] : 1.0 - t[0]) * (dj ? t[1] : 1.0 - t[1]) * (dk ? t[2] : 1.0 - t[2]);
|
|
678
|
+
acc += w * f.at(i0[0] + di, i0[1] + dj, i0[2] + dk);
|
|
679
|
+
}
|
|
680
|
+
// Manifold takes POSITIVE as inside. A distance field is the other
|
|
681
|
+
// way round, which is the common case, so the default flips it.
|
|
682
|
+
const double d = acc - isovalue;
|
|
683
|
+
return invert ? d : -d;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
manifold::Box bounds(manifold::vec3(origin[0], origin[1], origin[2]),
|
|
687
|
+
manifold::vec3((*hi)[0], (*hi)[1], (*hi)[2]));
|
|
688
|
+
// tolerance -1: a positive value makes Manifold do EXTRA evaluations per
|
|
689
|
+
// output vertex to snap nearer the true surface. Against a fixed grid
|
|
690
|
+
// those only re-interpolate data already used -- cost, no information.
|
|
691
|
+
// canParallel true: the lambda is pure C++ and never re-enters the
|
|
692
|
+
// evaluator, which is the whole point of taking a grid.
|
|
693
|
+
// canParallel is the crux: safe for a grid because the lambda is pure
|
|
694
|
+
// C++, and NOT safe for a function, which re-enters the evaluator.
|
|
695
|
+
// Manifold: parallel policies "will crash language runtimes with runtime
|
|
696
|
+
// locks that expect to not be called back by unregistered threads".
|
|
697
|
+
manifold::Manifold solid =
|
|
698
|
+
fieldFn ? manifold::Manifold::LevelSet(sampleFn, bounds, edge, 0.0, -1.0, /*canParallel=*/false)
|
|
699
|
+
: manifold::Manifold::LevelSet(sampleGrid, bounds, edge, 0.0, -1.0, /*canParallel=*/true);
|
|
700
|
+
|
|
701
|
+
if (solid.IsEmpty()) return {};
|
|
702
|
+
ColoredBody b;
|
|
703
|
+
b.body = std::move(solid);
|
|
704
|
+
std::vector<ColoredBody> out;
|
|
705
|
+
out.push_back(std::move(b));
|
|
706
|
+
return out;
|
|
707
|
+
}
|
|
708
|
+
|
|
454
709
|
} // namespace oscadeval
|
|
@@ -793,3 +793,230 @@ TEST(Simplify, NoChildrenIsEmpty) {
|
|
|
793
793
|
Evaluated e = evalSrc("simplify();");
|
|
794
794
|
EXPECT_TRUE(e.bodies.empty());
|
|
795
795
|
}
|
|
796
|
+
|
|
797
|
+
// -- levelset -------------------------------------------------------------
|
|
798
|
+
//
|
|
799
|
+
// A solid from a grid of sampled values. Takes a grid rather than an SDF
|
|
800
|
+
// callback: measured, building the field in script costs 0.41 us/sample
|
|
801
|
+
// against 0.96 us for a closure call, and a grid lets Manifold mesh in
|
|
802
|
+
// parallel because nothing re-enters the evaluator.
|
|
803
|
+
//
|
|
804
|
+
// Accuracy is bounded by the grid, not by Manifold -- it samples on a
|
|
805
|
+
// body-centred cubic lattice and the lambda interpolates -- so every
|
|
806
|
+
// tolerance here is a percentage, not an epsilon.
|
|
807
|
+
|
|
808
|
+
namespace {
|
|
809
|
+
|
|
810
|
+
// field[i][j][k] of the distance from the origin, over [-half, half]^3.
|
|
811
|
+
std::string sphereFieldSrc(int n, double half) {
|
|
812
|
+
return "N = " + std::to_string(n) + "; H = " + std::to_string(half) +
|
|
813
|
+
";\nfunction co(t) = -H + 2*H*t/(N-1);\n"
|
|
814
|
+
"f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
|
|
815
|
+
" sqrt(co(i)*co(i) + co(j)*co(j) + co(k)*co(k)) ]]];\n";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
std::vector<std::string> levelsetWarnings(const std::string& code) {
|
|
819
|
+
std::vector<std::string> out;
|
|
820
|
+
evalSrc(code, [&](const std::string& m) {
|
|
821
|
+
if (m.rfind("WARNING", 0) == 0) out.push_back(m);
|
|
822
|
+
});
|
|
823
|
+
return out;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
} // namespace
|
|
827
|
+
|
|
828
|
+
TEST(LevelSet, ASphereFieldGivesASphere) {
|
|
829
|
+
Evaluated e = evalSrc(sphereFieldSrc(50, 30) +
|
|
830
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
|
|
831
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
832
|
+
const double analytic = 4.0 / 3.0 * 3.14159265358979 * 20 * 20 * 20;
|
|
833
|
+
EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.01 * analytic); // within 1%
|
|
834
|
+
EXPECT_EQ(soleBody(e).Genus(), 0);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
TEST(LevelSet, AccuracyImprovesWithGridResolution) {
|
|
838
|
+
// The grid is the accuracy limit, so more samples must mean less error.
|
|
839
|
+
// Anything else means the resolution argument is not doing its job.
|
|
840
|
+
const double analytic = 4.0 / 3.0 * 3.14159265358979 * 20 * 20 * 20;
|
|
841
|
+
double prevErr = 1e30;
|
|
842
|
+
for (int n : {25, 40, 60}) {
|
|
843
|
+
Evaluated e = evalSrc(sphereFieldSrc(n, 30) +
|
|
844
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
|
|
845
|
+
ASSERT_EQ(e.bodies.size(), 1u) << "n=" << n;
|
|
846
|
+
const double err = std::abs(soleBody(e).Volume() - analytic);
|
|
847
|
+
EXPECT_LT(err, prevErr) << "n=" << n << " was no better than the coarser grid";
|
|
848
|
+
prevErr = err;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
TEST(LevelSet, ATorusFieldHasGenusOne) {
|
|
853
|
+
// The test that catches an implementation producing plausible-looking
|
|
854
|
+
// but topologically wrong output -- volume alone would not.
|
|
855
|
+
Evaluated e = evalSrc(
|
|
856
|
+
"N = 60; H = 30;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
|
|
857
|
+
"f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
|
|
858
|
+
" let(q = sqrt(co(i)*co(i) + co(j)*co(j)) - 15)\n"
|
|
859
|
+
" sqrt(q*q + co(k)*co(k)) ]]];\n"
|
|
860
|
+
"levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=6);");
|
|
861
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
862
|
+
EXPECT_EQ(soleBody(e).Genus(), 1);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
TEST(LevelSet, DisjointBlobsComeOutAsSeparateComponents) {
|
|
866
|
+
// Two spheres: Euler characteristic 4, so genus 1 - 4/2 = -1.
|
|
867
|
+
Evaluated e = evalSrc(
|
|
868
|
+
"N = 60; H = 30;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
|
|
869
|
+
"f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
|
|
870
|
+
" min(sqrt((co(i)-12)*(co(i)-12) + co(j)*co(j) + co(k)*co(k)),\n"
|
|
871
|
+
" sqrt((co(i)+12)*(co(i)+12) + co(j)*co(j) + co(k)*co(k))) ]]];\n"
|
|
872
|
+
"levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=8);");
|
|
873
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
874
|
+
EXPECT_EQ(soleBody(e).Genus(), -1);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
TEST(LevelSet, IndexOrderIsXThenYThenZ) {
|
|
878
|
+
// Deliberately asymmetric: a symmetric field passes even if the axes are
|
|
879
|
+
// transposed, which is the classic marching-cubes bug.
|
|
880
|
+
Evaluated e = evalSrc(
|
|
881
|
+
"N = 60; H = 30;\nfunction co(t) = -H + 2*H*t/(N-1);\n"
|
|
882
|
+
"f = [for (i=[0:N-1]) [for (j=[0:N-1]) [for (k=[0:N-1])\n"
|
|
883
|
+
" max(abs(co(i))/5, abs(co(j))/10, abs(co(k))/20) ]]];\n"
|
|
884
|
+
"levelset(f, bounds=[[-H,-H,-H],[H,H,H]], isovalue=1);");
|
|
885
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
886
|
+
const manifold::Box b = soleBody(e).BoundingBox();
|
|
887
|
+
EXPECT_NEAR(b.max.x, 5.0, 0.6);
|
|
888
|
+
EXPECT_NEAR(b.max.y, 10.0, 0.6);
|
|
889
|
+
EXPECT_NEAR(b.max.z, 20.0, 0.6);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
TEST(LevelSet, IsovalueSelectsTheSurface) {
|
|
893
|
+
Evaluated small = evalSrc(sphereFieldSrc(40, 30) +
|
|
894
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=10);");
|
|
895
|
+
Evaluated big = evalSrc(sphereFieldSrc(40, 30) +
|
|
896
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
|
|
897
|
+
EXPECT_LT(soleBody(small).Volume(), soleBody(big).Volume());
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
TEST(LevelSet, BoundsPlaceTheResult) {
|
|
901
|
+
Evaluated e = evalSrc(sphereFieldSrc(40, 30) +
|
|
902
|
+
"levelset(f, bounds=[[100,-30,-30],[160,30,30]], isovalue=20);");
|
|
903
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
904
|
+
const manifold::Box b = soleBody(e).BoundingBox();
|
|
905
|
+
EXPECT_NEAR((b.min.x + b.max.x) / 2, 130.0, 1.0);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
TEST(LevelSet, AnIsovalueNothingReachesIsEmpty) {
|
|
909
|
+
// A distance field is never negative, so nothing is inside. Not an
|
|
910
|
+
// error: asking is a fair question.
|
|
911
|
+
Evaluated e = evalSrc(sphereFieldSrc(20, 30) +
|
|
912
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=-1);");
|
|
913
|
+
EXPECT_TRUE(e.bodies.empty());
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
TEST(LevelSet, AnIsovalueEverythingSatisfiesFillsTheBounds) {
|
|
917
|
+
// The mirror case, and it is NOT empty: if every sample is inside, the
|
|
918
|
+
// solid is the whole box, clipped at the bounds. Worth pinning, because
|
|
919
|
+
// "nothing crossed the surface" and "everything is inside" look alike
|
|
920
|
+
// from the outside and mean opposite things.
|
|
921
|
+
Evaluated e = evalSrc(sphereFieldSrc(20, 30) +
|
|
922
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=1000);");
|
|
923
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
924
|
+
const manifold::Box b = soleBody(e).BoundingBox();
|
|
925
|
+
EXPECT_NEAR(b.max.x - b.min.x, 60.0, 1.0);
|
|
926
|
+
EXPECT_NEAR(soleBody(e).Volume(), 60.0 * 60.0 * 60.0, 0.02 * 60.0 * 60.0 * 60.0);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
TEST(LevelSet, MalformedFieldsWarn) {
|
|
930
|
+
for (const char* code : {
|
|
931
|
+
"levelset([[[1,2],[3,4]],[[5,6],[7]]], bounds=[[0,0,0],[1,1,1]]);", // ragged
|
|
932
|
+
"levelset([[[1,2],[3,\"x\"]],[[5,6],[7,8]]], bounds=[[0,0,0],[1,1,1]]);", // not numeric
|
|
933
|
+
"levelset(5, bounds=[[0,0,0],[1,1,1]]);", // not a field
|
|
934
|
+
"levelset([[[1,2],[3,4]],[[5,6],[7,8]]], bounds=[[0,0,0],[0,1,1]]);", // empty extent
|
|
935
|
+
}) {
|
|
936
|
+
EXPECT_FALSE(levelsetWarnings(code).empty()) << "silent on: " << code;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// -- levelset, function form ----------------------------------------------
|
|
941
|
+
//
|
|
942
|
+
// Same builtin, field given as function(x,y,z) instead of a grid. Slower --
|
|
943
|
+
// a closure call per sample, and canParallel must be false because the
|
|
944
|
+
// evaluator would otherwise be called back from Manifold's worker threads --
|
|
945
|
+
// but MORE accurate, because Manifold picks its own sample points and can
|
|
946
|
+
// snap toward the true surface instead of interpolating a fixed lattice.
|
|
947
|
+
//
|
|
948
|
+
// Measured at matched resolution, sphere against analytic:
|
|
949
|
+
// 50^3 grid 0.23s (-0.244%) function 0.32s (-0.103%)
|
|
950
|
+
// 100^3 grid 1.06s (-0.059%) function 1.91s (-0.025%)
|
|
951
|
+
|
|
952
|
+
TEST(LevelSetFn, AFunctionFieldGivesTheSameSphere) {
|
|
953
|
+
Evaluated e = evalSrc(
|
|
954
|
+
"levelset(function(x,y,z) sqrt(x*x+y*y+z*z), "
|
|
955
|
+
"bounds=[[-30,-30,-30],[30,30,30]], isovalue=20, edge=1.5);");
|
|
956
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
957
|
+
const double analytic = 4.0 / 3.0 * 3.14159265358979 * 20 * 20 * 20;
|
|
958
|
+
EXPECT_NEAR(soleBody(e).Volume(), analytic, 0.01 * analytic);
|
|
959
|
+
EXPECT_EQ(soleBody(e).Genus(), 0);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
TEST(LevelSetFn, AgreesWithTheGridFormOnTheSameField) {
|
|
963
|
+
// The two intakes must describe the same solid; they differ only in how
|
|
964
|
+
// finely they can resolve it.
|
|
965
|
+
Evaluated grid = evalSrc(sphereFieldSrc(50, 30) +
|
|
966
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=20);");
|
|
967
|
+
Evaluated fn = evalSrc(
|
|
968
|
+
"levelset(function(x,y,z) sqrt(x*x+y*y+z*z), "
|
|
969
|
+
"bounds=[[-30,-30,-30],[30,30,30]], isovalue=20, edge=" +
|
|
970
|
+
std::to_string(60.0 / 49.0) + ");");
|
|
971
|
+
ASSERT_EQ(grid.bodies.size(), 1u);
|
|
972
|
+
ASSERT_EQ(fn.bodies.size(), 1u);
|
|
973
|
+
EXPECT_EQ(soleBody(fn).Genus(), soleBody(grid).Genus());
|
|
974
|
+
EXPECT_NEAR(soleBody(fn).Volume(), soleBody(grid).Volume(),
|
|
975
|
+
0.01 * soleBody(grid).Volume());
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
TEST(LevelSetFn, TopologyIsFoundFromAFunctionToo) {
|
|
979
|
+
Evaluated e = evalSrc(
|
|
980
|
+
"levelset(function(x,y,z) let(q = sqrt(x*x+y*y) - 15) sqrt(q*q + z*z), "
|
|
981
|
+
"bounds=[[-30,-30,-30],[30,30,30]], isovalue=6, edge=1.2);");
|
|
982
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
983
|
+
EXPECT_EQ(soleBody(e).Genus(), 1);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
TEST(LevelSetFn, AFunctionFieldRequiresEdge) {
|
|
987
|
+
// There is no grid to infer spacing from, and the cost is CUBIC in it --
|
|
988
|
+
// guessing would silently produce either a useless mesh or a ten-minute
|
|
989
|
+
// one. Making the caller say is the safer failure.
|
|
990
|
+
const std::vector<std::string> w = levelsetWarnings(
|
|
991
|
+
"levelset(function(x,y,z) sqrt(x*x+y*y+z*z), bounds=[[-1,-1,-1],[1,1,1]]);");
|
|
992
|
+
ASSERT_EQ(w.size(), 1u);
|
|
993
|
+
EXPECT_NE(w[0].find("edge="), std::string::npos) << w[0];
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
TEST(LevelSetFn, AFunctionOfTheWrongArityWarns) {
|
|
997
|
+
const std::vector<std::string> w = levelsetWarnings(
|
|
998
|
+
"levelset(function(p) 1, bounds=[[-1,-1,-1],[1,1,1]], edge=0.5);");
|
|
999
|
+
ASSERT_EQ(w.size(), 1u);
|
|
1000
|
+
EXPECT_NE(w[0].find("three parameters"), std::string::npos) << w[0];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
TEST(LevelSetFn, AFieldFunctionCanCaptureOuterVariables) {
|
|
1004
|
+
Evaluated e = evalSrc(
|
|
1005
|
+
"r = 20;\nlevelset(function(x,y,z) sqrt(x*x+y*y+z*z), "
|
|
1006
|
+
"bounds=[[-30,-30,-30],[30,30,30]], isovalue=r, edge=1.5);");
|
|
1007
|
+
ASSERT_EQ(e.bodies.size(), 1u);
|
|
1008
|
+
EXPECT_NEAR(soleBody(e).Volume(), 4.0 / 3.0 * 3.14159265358979 * 8000, 400.0);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
TEST(LevelSet, AnExplicitlyUndefIsovalueFallsBackToTheDefault) {
|
|
1012
|
+
// Same rule as linear_solve's b, and for the same reason: a wrapper
|
|
1013
|
+
// with a fixed signature forwards every parameter, undef included.
|
|
1014
|
+
Evaluated withUndef = evalSrc(sphereFieldSrc(30, 30) +
|
|
1015
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=undef);");
|
|
1016
|
+
Evaluated withZero = evalSrc(sphereFieldSrc(30, 30) +
|
|
1017
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=0);");
|
|
1018
|
+
EXPECT_EQ(withUndef.bodies.size(), withZero.bodies.size());
|
|
1019
|
+
EXPECT_TRUE(levelsetWarnings(sphereFieldSrc(30, 30) +
|
|
1020
|
+
"levelset(f, bounds=[[-30,-30,-30],[30,30,30]], isovalue=undef);")
|
|
1021
|
+
.empty());
|
|
1022
|
+
}
|
{openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_feature_detection.cpp
RENAMED
|
@@ -35,7 +35,7 @@ std::vector<std::string> echoesFrom(const std::string& src) {
|
|
|
35
35
|
// in featureLevels() -- shows up as a 0.
|
|
36
36
|
const char* kFeatures[] = {"separate-children", "minkowski-diff", "sphere-styles", "export-name",
|
|
37
37
|
"simplify-op", "expr-import", "object-function", "roof-op",
|
|
38
|
-
"render-expr", "polyhedron-vnf", "linear-solve"};
|
|
38
|
+
"render-expr", "polyhedron-vnf", "linear-solve", "levelset"};
|
|
39
39
|
|
|
40
40
|
} // namespace
|
|
41
41
|
|
{openscad_cpp_evaluator-0.49.0 → openscad_cpp_evaluator-0.50.1}/tests/test_function_builtins.cpp
RENAMED
|
@@ -883,3 +883,17 @@ TEST(LinearSolve, WorksUnderBothEngines) {
|
|
|
883
883
|
}
|
|
884
884
|
}
|
|
885
885
|
|
|
886
|
+
|
|
887
|
+
TEST(LinearSolve, AnExplicitlyUndefRightHandSideCountsAsAbsent) {
|
|
888
|
+
// BOSL2's builtins.scad pattern is a wrapper with a fixed signature:
|
|
889
|
+
// function _linear_solve(A, b) = linear_solve(A, b);
|
|
890
|
+
// so a determinant()-style caller that passes no right-hand side still
|
|
891
|
+
// forwards b as undef. Treating that as a BAD argument would warn on
|
|
892
|
+
// every such call. Passing undef is idiomatically the same as not
|
|
893
|
+
// passing in OpenSCAD -- BOSL2 threads optional arguments through
|
|
894
|
+
// wrappers that way throughout.
|
|
895
|
+
Evaluator ev;
|
|
896
|
+
EXPECT_NEAR(asNum(evalSrc("linear_solve([[2,1],[1,3]], undef).det", ev)), 5.0, 1e-12);
|
|
897
|
+
EXPECT_TRUE(isUndef(evalSrc("linear_solve([[2,1],[1,3]], undef).x", ev)));
|
|
898
|
+
EXPECT_TRUE(builtinWarnings("linear_solve([[2,1],[1,3]], undef)").empty());
|
|
899
|
+
}
|
|
File without changes
|