diff --git a/.github/workflows/conan-package.yml b/.github/workflows/package.yml similarity index 59% rename from .github/workflows/conan-package.yml rename to .github/workflows/package.yml index 9c62131..cc7898e 100644 --- a/.github/workflows/conan-package.yml +++ b/.github/workflows/package.yml @@ -1,4 +1,4 @@ -name: conan-package +name: package on: push: @@ -12,8 +12,9 @@ on: - 'conandata.yml' - 'CMakeLists.txt' - 'requirements.txt' - - '.github/workflows/conan-package.yml' + - '.github/workflows/package.yml' - '.github/workflows/requirements*' + - 'libnest2d_js/**' branches: - main - 'CURA-*' @@ -25,4 +26,13 @@ on: jobs: conan-package: uses: ultimaker/cura-workflows/.github/workflows/conan-package.yml@main + with: + platform_wasm: true secrets: inherit + + npm-package: + needs: [ conan-package ] + uses: ultimaker/cura-workflows/.github/workflows/npm-package.yml@main + with: + package_version_full: ${{ needs.conan-package.outputs.package_version_full }} + secrets: inherit \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 0baf41e..1ccf762 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,10 +2,12 @@ cmake_minimum_required(VERSION 3.20) cmake_policy(SET CMP0091 NEW) project(libnest2d) find_package(standardprojectsettings REQUIRED) +find_package(spdlog REQUIRED) option(BUILD_SHARED_LIBS "Build shared libs instead of static (applies for dependencies as well)" OFF) option(HEADER_ONLY "If enabled static library will not be built." ON) option(ENABLE_TESTING "Build with Google unittest" OFF) +option(WITH_JS_BINDINGS "Build JS/WASM bindings for npm package" OFF) set(GEOMETRIES clipper CACHE STRING "Geometry backend, available options: 'clipper' (default), 'boost'") set(OPTIMIZER nlopt CACHE STRING "Optimization backend, available options: 'nlopt' (default), 'optimlib'") set(THREADING std CACHE STRING "Multithreading, available options: 'std' (default), 'tbb', 'omp', 'none'") @@ -54,7 +56,7 @@ target_compile_definitions(project_options INTERFACE LIBNEST2D_GEOMETRIES_${GEOM if("${OPTIMIZER}" STREQUAL "nlopt") find_package(NLopt REQUIRED) - target_link_libraries(project_options INTERFACE NLopt::nlopt) + target_link_libraries(project_options INTERFACE NLopt::nlopt spdlog::spdlog) list(APPEND nest2d_HDRS include/libnest2d/optimizers/nlopt/simplex.hpp include/libnest2d/optimizers/nlopt/subplex.hpp @@ -89,17 +91,22 @@ endif() target_compile_definitions(project_options INTERFACE LIBNEST2D_THREADING_${THREADING}) set(libnest2d_SRCS - src/libnest2d.cpp - ) + src/libnest2d.cpp +) + +if(WITH_JS_BINDINGS OR EMSCRIPTEN) + add_subdirectory(libnest2d_js) +endif() + if(HEADER_ONLY) add_library(nest2d INTERFACE ${libnest2d_HDRS}) target_link_libraries(nest2d INTERFACE project_options) target_include_directories(nest2d - INTERFACE - $ - $ - ) + INTERFACE + $ + $ + ) else() if(BUILD_SHARED_LIBS) add_library(nest2d SHARED ${libnest2d_SRCS} ${libnest2d_HDRS}) @@ -111,12 +118,12 @@ else() endif() target_link_libraries(nest2d PUBLIC project_options) target_include_directories(nest2d - PUBLIC - $ - $ - PRIVATE - $ - ) + PUBLIC + $ + $ + PRIVATE + $ + ) endif() if(ENABLE_TESTING) diff --git a/conanfile.py b/conanfile.py index 5fa545b..bebf606 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,6 +15,8 @@ class Nest2DConan(ConanFile): name = "nest2d" + author = "UltiMaker" + url = "https://github.com/Ultimaker/libnest2d" description = "2D irregular bin packaging and nesting library written in modern C++" topics = ("conan", "cura", "prusaslicer", "nesting", "c++", "bin packaging") settings = "os", "compiler", "build_type", "arch" @@ -22,13 +24,15 @@ class Nest2DConan(ConanFile): package_type = "library" implements = ["auto_header_only"] + python_requires = "npmpackage/[>=1.0.0]" options = { "shared": [True, False], "fPIC": [True, False], "header_only": [True, False], "geometries": ["clipper", "boost"], "optimizer": ["nlopt", "optimlib"], - "threading": ["std", "tbb", "omp", "none"] + "threading": ["std", "tbb", "omp", "none"], + "with_js_bindings": [True, False] } default_options = { "shared": True, @@ -36,7 +40,8 @@ class Nest2DConan(ConanFile): "header_only": False, "geometries": "clipper", "optimizer": "nlopt", - "threading": "std" + "threading": "std", + "with_js_bindings": False } def set_version(self): @@ -67,12 +72,24 @@ def export_sources(self): copy(self, "*", path.join(self.recipe_folder, "include"), path.join(self.export_sources_folder, "include")) copy(self, "*", path.join(self.recipe_folder, "tests"), path.join(self.export_sources_folder, "tests")) copy(self, "*", path.join(self.recipe_folder, "tools"), path.join(self.export_sources_folder, "tools")) + copy(self, "*", path.join(self.recipe_folder, "libnest2d_js"), + os.path.join(self.export_sources_folder, "libnest2d_js")) def layout(self): cmake_layout(self) + self.cpp.build.bin = [] + self.cpp.build.bindirs = [] + self.cpp.package.bindirs = ["bin"] self.cpp.package.libs = ["nest2d"] + if self.settings.os == "Emscripten": + self.cpp.build.bin = ["libnest2d_js.js"] + self.cpp.package.bin = ["libnest2d_js.js"] + self.cpp.build.bindirs += ["libnest2d_js"] + + self.cpp.package.includedirs = ["include"] def requirements(self): + self.requires("spdlog/[>=1.14.1]", transitive_headers=True) if self.options.geometries == "clipper": self.requires("clipper/6.4.2@ultimaker/stable", transitive_headers=True) if self.options.geometries == "boost" or self.options.geometries == "clipper": @@ -133,6 +150,7 @@ def generate(self): tc.variables["GEOMETRIES"] = self.options.geometries tc.variables["OPTIMIZER"] = self.options.optimizer tc.variables["THREADING"] = self.options.threading + tc.variables["WITH_JS_BINDINGS"] = self.options.get_safe("with_js_bindings", False) tc.generate() @@ -142,7 +160,19 @@ def build(self): cmake.build() cmake.install() + def deploy(self): + if self.settings.os == "Emscripten" or self.options.get_safe("with_js_bindings", False): + copy(self, "libnest2d_js*", src=os.path.join(self.package_folder, "bin"), dst=self.install_folder) + copy(self, "*", src=os.path.join(self.package_folder, "bin"), dst=self.install_folder) + def package(self): + + if self.settings.os == "Emscripten" or self.options.get_safe("with_js_bindings", False): + copy(self, pattern="libnest2d_js*", src=os.path.join(self.build_folder, "libnest2d_js"), + dst=os.path.join(self.package_folder, "bin")) + copy(self, f"*.d.ts", src=self.build_folder, dst=os.path.join(self.package_folder, "bin"), keep_path = False) + copy(self, f"*.js", src=self.build_folder, dst=os.path.join(self.package_folder, "bin"), keep_path = False) + copy(self, f"*.wasm", src=self.build_folder, dst=os.path.join(self.package_folder, "bin"), keep_path = False) packager = AutoPackager(self) packager.run() @@ -162,3 +192,10 @@ def package_info(self): self.cpp_info.defines.append(f"LIBNEST2D_THREADING_{self.options.threading}") if self.settings.os in ["Linux", "FreeBSD", "Macos"] and self.options.threading == "std": self.cpp_info.system_libs.append("pthread") + + # npm package json for Emscripten builds + if self.settings.os == "Emscripten" or self.options.get_safe("with_js_bindings", False): + self.python_requires["npmpackage"].module.conf_package_json(self) + # Expose the path to the JS/WASM assets for consumers + js_asset_path = os.path.join(self.package_folder, "bin") + self.conf_info.define("user.nest2d:js_path", js_asset_path) diff --git a/include/libnest2d/parallel.hpp b/include/libnest2d/parallel.hpp index 2eaf86c..6f1f59e 100644 --- a/include/libnest2d/parallel.hpp +++ b/include/libnest2d/parallel.hpp @@ -14,7 +14,7 @@ #endif namespace libnest2d { namespace __parallel { - + template using TIteratorValue = typename std::iterator_traits::value_type; @@ -56,8 +56,14 @@ inline void enumerate( for(TN fi = 0; fi < N; ++fi) rets[fi].wait(); #endif + +#ifdef __EMSCRIPTEN__ + // For WASM/Emscripten builds, always use non-parallel execution + // due to limited threading support in WebAssembly + for(TN n = 0; n < N; n++) fn(*(from + n), n); +#endif } }} -#endif //LIBNEST2D_PARALLEL_HPP +#endif //LIBNEST2D_PARALLEL_HPP \ No newline at end of file diff --git a/libnest2d_js/CMakeLists.txt b/libnest2d_js/CMakeLists.txt new file mode 100644 index 0000000..b5bf699 --- /dev/null +++ b/libnest2d_js/CMakeLists.txt @@ -0,0 +1,64 @@ +message(STATUS "Building for Emscripten") +cmake_minimum_required(VERSION 3.10) +project(libnest2d_js) + +add_executable(libnest2d_js libnest2d_js.cpp) + +if (NOT CMAKE_CXX_PLATFORM_ID STREQUAL "emscripten") + use_threads(libnest2d_js) +endif () + +# Emscripten bindings +set_target_properties(libnest2d_js PROPERTIES LINK_FLAGS "--bind") + +# Find Clipper library (required for polyclipping/clipper.hpp) +find_package(clipper REQUIRED) + +# Find Boost library (required for boost/geometry.hpp) +find_package(Boost REQUIRED) + +find_package(NLopt REQUIRED) + +# Include directories +target_include_directories(libnest2d_js PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${CMAKE_CURRENT_SOURCE_DIR}/../src + ${clipper_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} +) + +# Link Boost headers +target_link_libraries(libnest2d_js PRIVATE Boost::headers) + +# Link Clipper library +target_link_libraries(libnest2d_js PRIVATE clipper::clipper) + +target_link_libraries(libnest2d_js PRIVATE NLopt::nlopt) + +target_link_libraries(libnest2d_js PUBLIC spdlog::spdlog) +# Define backend macro for Emscripten +target_compile_definitions(libnest2d_js PRIVATE LIBNEST2D_GEOMETRIES_clipper) +target_compile_definitions(libnest2d_js PRIVATE LIBNEST2D_OPTIMIZER_nlopt) + +# Emscripten-specific options (optional, but recommended) +target_link_options(libnest2d_js + PUBLIC + "SHELL:-s USE_ES6_IMPORT_META=1" + "SHELL:-s FORCE_FILESYSTEM=1" + "SHELL:-s EXPORT_NAME=libnest2d_js" + "SHELL:-s MODULARIZE=1" + "SHELL:-s EXPORT_ES6=1" + "SHELL:-s SINGLE_FILE=1" + "SHELL:-s ALLOW_MEMORY_GROWTH=1" + "SHELL:-s ERROR_ON_UNDEFINED_SYMBOLS=0" + "SHELL:--bind" + "SHELL:-l embind" + "SHELL: --emit-tsd libnest2d_js.d.ts" + "SHELL:-sWASM_BIGINT=1" + "SHELL:-sASSERTIONS=1" + +) + +# If you want to enable debug options, add them conditionally: +# $<$:SHELL:-g3> +# $<$:SHELL:-gsource-map> diff --git a/libnest2d_js/libnest2d_js.cpp b/libnest2d_js/libnest2d_js.cpp new file mode 100644 index 0000000..a5ea31f --- /dev/null +++ b/libnest2d_js/libnest2d_js.cpp @@ -0,0 +1,182 @@ +#ifndef LIBNEST2D_JS_H +#define LIBNEST2D_JS_H +//Copyright (c) 2022 Ultimaker B.V. +//libnest2d_js is released under the terms of the LGPLv3 or higher. + +// Emscripten Embind bindings for libnest2d +#include +#include +#include +#include +#include + +using namespace emscripten; +using namespace libnest2d; +using namespace placers; + +// Type aliases to match Python bindings +using Point = PointImpl; +using Box = _Box; +using Circle = _Circle; +using Item = _Item; +using NfpConfig = NfpPConfig; +using Polygon = PolygonImpl; + +// Add aliases for angle types +using Radians = libnest2d::Radians; +using Degrees = libnest2d::Degrees; + +// Declare value types for TypeScript generation +EMSCRIPTEN_DECLARE_VAL_TYPE(PointList); +EMSCRIPTEN_DECLARE_VAL_TYPE(ItemList); +EMSCRIPTEN_DECLARE_VAL_TYPE(DoubleList); +EMSCRIPTEN_DECLARE_VAL_TYPE(ResultAndItem); + +ResultAndItem resultAndItems(const size_t result, const ItemList& items) { + emscripten::val obj = emscripten::val::object(); + obj.set("result", result); + obj.set("items", items); + return ResultAndItem { obj }; +} + +// Wrapper function for nest() to handle JavaScript arrays +ResultAndItem nestWrapper(const ItemList& jsItems, const Box& bin, double scaleFactor) { + // Convert JavaScript array to std::vector + std::vector items; + auto length = jsItems["length"].as(); + items.reserve(length); + + for (unsigned i = 0; i < length; i++) { + Item item = jsItems[i].as(); + items.push_back(item); + } + + NestConfig<> cfg; + cfg.placer_config.rotations = { 0 }; + + size_t result = nest(items, bin, scaleFactor, cfg); + + emscripten::val jsItemsResult = emscripten::val::array(); + // Copy results back to original JavaScript items + for (size_t i = 0; i < items.size() && i < length; ++i) { + jsItemsResult.set(i, items[i]); + } + + return resultAndItems(result, ItemList(jsItemsResult)); +} + +EMSCRIPTEN_BINDINGS(libnest2d_js) { + // Register TypeScript types for arrays + emscripten::register_type("Point[]"); + emscripten::register_type("Item[]"); + emscripten::register_type("number[]"); + emscripten::register_type("{ result: number, items: Item[] }"); + + // Point class - fix the getter/setter type issue + class_("Point") + .constructor<>() + .constructor() + .function("x", optional_override([](const Point& self) -> long { return getX(self); })) + .function("y", optional_override([](const Point& self) -> long { return getY(self); })) + .function("setX", optional_override([](Point& self, long value) { setX(self, value); })) + .function("setY", optional_override([](Point& self, long value) { setY(self, value); })) + ; + + // Box class + class_("Box") + .constructor() + .constructor() + .constructor() + .class_function("infinite", &Box::infinite) + .class_function("fromDimensions", optional_override([](long width, long height) -> Box { + return Box(width, height); + })) + .function("minCorner", select_overload(&Box::minCorner)) + .function("maxCorner", select_overload(&Box::maxCorner)) + .function("width", &Box::width) + .function("height", &Box::height) + .function("area", &Box::area) + .function("center", select_overload(&Box::center)) + ; + + // Circle class + class_("Circle") + .constructor<>() + .constructor() + .function("center", select_overload(&Circle::center)) + .function("setCenter", select_overload(&Circle::center)) + .function("radius", select_overload(&Circle::radius)) + .function("setRadius", select_overload(&Circle::radius)) + .function("area", &Circle::area) + ; + + // NfpConfig::Alignment enum + enum_("Alignment") + .value("CENTER", NfpConfig::Alignment::CENTER) + .value("BOTTOM_LEFT", NfpConfig::Alignment::BOTTOM_LEFT) + .value("BOTTOM_RIGHT", NfpConfig::Alignment::BOTTOM_RIGHT) + .value("TOP_LEFT", NfpConfig::Alignment::TOP_LEFT) + .value("TOP_RIGHT", NfpConfig::Alignment::TOP_RIGHT) + .value("DONT_ALIGN", NfpConfig::Alignment::DONT_ALIGN) + ; + + // NfpConfig class + emscripten::value_object("NfpConfig") + .field("alignment", &NfpConfig::alignment) + .field("starting_point", &NfpConfig::starting_point) + .field("accuracy", &NfpConfig::accuracy) + .field("explore_holes", &NfpConfig::explore_holes) + .field("parallel", &NfpConfig::parallel) + ; + + // Item class + class_("Item") + .constructor() + .class_function("createFromVertices", optional_override([](const PointList& vertices) -> Item { + PolygonImpl polygon; + polygon.Contour = emscripten::vecFromJSArray(vertices); + return Item(polygon); + })) + .function("binId", select_overload(&Item::binId)) + .function("setBinId", select_overload(&Item::binId)) + .function("area", &Item::area) + .function("vertexCount", &Item::vertexCount) + .function("boundingBox", &Item::boundingBox) + .function("translate", &Item::translate) + .function("rotate", &Item::rotate) + .function("rotation", optional_override([](const Item& self) { return self.rotation(); })) + .function("translation", optional_override([](const Item& self) { return self.translation(); })) + .function("isFixed", &Item::isFixed) + .function("isDisallowedArea", &Item::isDisallowedArea) + .function("markAsFixedInBin", &Item::markAsFixedInBin) + .function("markAsDisallowedAreaInBin", &Item::markAsDisallowedAreaInBin) + .function("priority", select_overload(&Item::priority)) + .function("setPriority", select_overload(&Item::priority)) + .function("transformedShape", optional_override([](const Item& self) { + const auto& poly = self.transformedShape(); + emscripten::val shape = emscripten::val::array(); + for (const auto& point : poly.Contour) { + shape.call("push", point); + } + return PointList { shape }; + })); + + + // Polygon class for internal type compatibility + class_("Polygon"); + + // Radians class for rotation angles + class_("Radians") + .constructor() + .function("toDegrees", &Radians::toDegrees); + + // Degrees class for rotation angles + class_("Degrees") + .constructor() + .function("toRadians", &Degrees::toRadians); + + // Main nest function + function("nest", &nestWrapper); +} + +#endif // LIBNEST2D_JS_H