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