Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f8473f0
Implement Emscripten support with JavaScript bindings for `UvulaJS`
saumyaj3 Dec 12, 2025
1737a91
Update GitHub workflow naming: `conan-package.yml` → `package.yml`
saumyaj3 Dec 12, 2025
d3f77fe
Fix workflow path trigger in package.yml to include correct location
saumyaj3 Dec 12, 2025
77e4a0d
Refactor UvulaJS JavaScript bindings for TypeScript integration
saumyaj3 Dec 16, 2025
02cb395
Expand GitHub Actions path triggers in `package.yml` to include `Uvul…
saumyaj3 Dec 16, 2025
3288882
Refactor `UvulaJS` bindings to enhance TypeScript compatibility
saumyaj3 Dec 16, 2025
2422f59
Add Emscripten-specific threading configuration and refactor memory a…
saumyaj3 Dec 16, 2025
d46eaba
Simplify bindings
casperlamboo Jan 11, 2026
8c483b0
Transpose camera matrix
casperlamboo Jan 12, 2026
229a2da
Merge branch 'main' into NP-1250-create-emscripten-binding-II
saumyaj3 Jan 12, 2026
63a340d
Merge remote-tracking branch 'origin/NP-1250-create-emscripten-bindin…
casperlamboo Jan 13, 2026
7081897
Merge pull request #6 from Ultimaker/NP-1250-create-emscripten-bindin…
casperlamboo Jan 13, 2026
202d95c
Construct `Float32Array` to return uv coords
casperlamboo Jan 14, 2026
2516aad
Move vertices in `Geometry` container
casperlamboo Jan 14, 2026
a315f3a
Merge pull request #8 from Ultimaker/NP-1250-create-emscripten-bindin…
casperlamboo Jan 15, 2026
6332d8b
Fix js bindings so we can provide a `const Geometry&` in `project` fu…
casperlamboo Jan 15, 2026
cf3190e
Merge branch 'refs/heads/NP-1250-create-emscripten-binding-III' into …
casperlamboo Jan 15, 2026
35aed92
Fix Python binding
wawanbreton Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: conan-package
name: package

on:
push:
Expand All @@ -10,11 +10,13 @@ on:
- 'conanfile.py'
- 'conandata.yml'
- 'CMakeLists.txt'
- '.github/workflows/conan-package.yml'
- '.github/workflows/package.yml'
- 'UvulaJS/**'
branches:
- main
- master
- 'CURA-*'
- 'NP-*'
- 'PP-*'
- '[0-9].[0-9]*'
- '[0-9].[0-9][0-9]*'
Expand All @@ -25,4 +27,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
20 changes: 18 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ if (WITH_PYTHON_BINDINGS)
message(STATUS "Configuring pyUvula version: ${PYUVULA_VERSION}")
endif ()

option(WITH_JS_BINDINGS "Build with JavaScript/Emscripten bindings: `UvulaJS`" OFF)
if (WITH_JS_BINDINGS)
message(STATUS "Configuring UvulaJS with Emscripten bindings")
endif ()

if (NOT DEFINED UVULA_VERSION)
message(FATAL_ERROR "UVULA_VERSION is not defined!")
endif ()
Expand Down Expand Up @@ -51,8 +56,14 @@ target_compile_definitions(libuvula
UVULA_VERSION="${UVULA_VERSION}"
)

use_threads(libuvula)
enable_sanitizers(libuvula)
if(NOT EMSCRIPTEN)
use_threads(libuvula)
endif()
if(EMSCRIPTEN)
# Skip sanitizers and threading for Emscripten builds to avoid shared memory issues
else()
enable_sanitizers(libuvula)
endif()
if (${EXTENSIVE_WARNINGS})
set_project_warnings(libuvula)
endif ()
Expand All @@ -62,6 +73,11 @@ if (WITH_PYTHON_BINDINGS)
add_subdirectory(pyUvula)
endif ()

# --- Setup JavaScript/Emscripten bindings ---
if (WITH_JS_BINDINGS)
add_subdirectory(UvulaJS)
endif ()

# --- Setup command line interface (for testing purposes) ---
if (WITH_CLI)
add_subdirectory(cli)
Expand Down
16 changes: 16 additions & 0 deletions UvulaJS/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
add_executable(uvula_js UvulaJS.cpp)
target_link_options(uvula_js
PUBLIC
"SHELL:-s USE_ES6_IMPORT_META=1"
"SHELL:-s FORCE_FILESYSTEM=1"
"SHELL:-s EXPORT_NAME=uvula"
"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 uvula_js.d.ts"
)
target_link_libraries(uvula_js PUBLIC libuvula)
260 changes: 260 additions & 0 deletions UvulaJS/UvulaJS.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// (c) 2025, UltiMaker -- see LICENCE for details

#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <vector>

#include "Face.h"
#include "Matrix44F.h"
#include "Point2F.h"
#include "Point3F.h"
#include "Vector3F.h"
#include "project.h"
#include "unwrap.h"

using namespace emscripten;

// TypeScript type aliases for better type safety
EMSCRIPTEN_DECLARE_VAL_TYPE(Float32Array);
EMSCRIPTEN_DECLARE_VAL_TYPE(Uint32Array);
EMSCRIPTEN_DECLARE_VAL_TYPE(Int32Array);
EMSCRIPTEN_DECLARE_VAL_TYPE(UInt32Array);
EMSCRIPTEN_DECLARE_VAL_TYPE(PolygonArray);

// Return type for unwrap function
struct UnwrapResult
{
Float32Array uvCoordinates = Float32Array{emscripten::val::array()};
uint32_t textureWidth;
uint32_t textureHeight;
};

struct Geometry
{
std::vector<Point3F> vertices;
std::vector<Face> indices;
std::vector<Point2F> uvs;
std::vector<FaceSigned> connectivity;

Geometry(const Float32Array& vertices_js, const Uint32Array& indices_js, const Float32Array& uvs_js, const Int32Array& connectivity_js)
{
// Convert vertices
auto vertices_length = vertices_js["length"].as<int>();
vertices.reserve(vertices_length / 3);
for (int i = 0; i < vertices_length; i += 3) {
auto x = vertices_js[i].as<float>();
auto y = vertices_js[i + 1].as<float>();
auto z = vertices_js[i + 2].as<float>();
vertices.emplace_back(x, y, z);
}

// Convert indices
auto indices_length = indices_js["length"].as<int>();
indices.reserve(indices_length / 3);
for (int i = 0; i < indices_length; i += 3)
{
auto i1 = indices_js[i].as<uint32_t>();
auto i2 = indices_js[i + 1].as<uint32_t>();
auto i3 = indices_js[i + 2].as<uint32_t>();
indices.push_back({i1, i2, i3});
}

// Convert UVs
auto uvs_length = uvs_js["length"].as<int>();
uvs.reserve(uvs_length / 2);
for (int i = 0; i < uvs_length; i += 2) {
auto u = uvs_js[i].as<float>();
auto v = uvs_js[i + 1].as<float>();
uvs.emplace_back(u, v);
}

// Convert connectivity
auto connectivity_length = connectivity_js["length"].as<int>();
connectivity.reserve(connectivity_length / 3);
for (int i = 0; i < connectivity_length; i += 3) {
auto i1 = connectivity_js[i].as<int32_t>();
auto i2 = connectivity_js[i + 1].as<int32_t>();
auto i3 = connectivity_js[i + 2].as<int32_t>();
connectivity.push_back({i1, i2, i3});
}
}
};

std::string get_uvula_info()
{
return { UVULA_VERSION };
}

// Typed wrapper functions for better TypeScript generation
UnwrapResult unwrap(const Float32Array& vertices_js, const Uint32Array& indices_js)
{
// Convert JavaScript arrays to C++ vectors
std::vector<Point3F> vertex_points;
std::vector<Face> face_indices;

// Get array lengths
unsigned vertices_length = vertices_js["length"].as<unsigned>();
unsigned indices_length = indices_js["length"].as<unsigned>();

// Convert vertices (expecting flat array of [x1, y1, z1, x2, y2, z2, ...])
vertex_points.reserve(vertices_length / 3);
for (unsigned i = 0; i < vertices_length; i += 3) {
float x = vertices_js[i].as<float>();
float y = vertices_js[i + 1].as<float>();
float z = vertices_js[i + 2].as<float>();
vertex_points.emplace_back(x, y, z);
}

// Convert indices (expecting flat array of [i1, i2, i3, i4, i5, i6, ...])
face_indices.reserve(indices_length / 3);
for (unsigned i = 0; i < indices_length; i += 3)
{
uint32_t i1 = indices_js[i].as<uint32_t>();
uint32_t i2 = indices_js[i + 1].as<uint32_t>();
uint32_t i3 = indices_js[i + 2].as<uint32_t>();
face_indices.push_back({i1, i2, i3});
}

// Prepare output
std::vector<Point2F> uv_coords(vertex_points.size(), {0.0f, 0.0f});
uint32_t texture_width;
uint32_t texture_height;

// Perform unwrapping
bool success = smartUnwrap(vertex_points, face_indices, uv_coords, texture_width, texture_height);

if (!success)
{
throw std::runtime_error("Couldn't unwrap UVs!");
}

// Convert result to structured return type
emscripten::val uv_array = emscripten::val::global("Float32Array").new_(uv_coords.size() * 2);
for (size_t i = 0; i < uv_coords.size(); ++i)
{
uv_array.set(i * 2, uv_coords[i].x);
uv_array.set(i * 2 + 1, uv_coords[i].y);
}

return UnwrapResult{
.uvCoordinates = Float32Array{uv_array},
.textureWidth = texture_width,
.textureHeight = texture_height
};
}

PolygonArray project(
const Float32Array& stroke_polygon_js,
const Geometry& geometry,
uint32_t texture_width,
uint32_t texture_height,
const Float32Array& camera_projection_matrix_js,
bool is_camera_perspective,
uint32_t viewport_width,
uint32_t viewport_height,
const Vector3F& camera_normal,
uint32_t face_id
)
{
std::vector<Point2F> stroke_points;
auto stroke_length = stroke_polygon_js["length"].as<int>();
stroke_points.reserve(stroke_length / 2);
for (int i = 0; i < stroke_length; i += 2) {
auto x = stroke_polygon_js[i].as<float>();
auto y = stroke_polygon_js[i + 1].as<float>();
stroke_points.push_back({x, y});
}

// Convert camera projection matrix (4x4 matrix as flat array)
float matrix_data[4][4];
for (int i = 0; i < 16; ++i) {
matrix_data[i % 4][i / 4] = camera_projection_matrix_js[i].as<float>();
}
Matrix44F projection_matrix(matrix_data);

// Call the projection function
std::vector<Polygon> result = doProject(
stroke_points,
geometry.vertices,
geometry.indices,
geometry.uvs,
geometry.connectivity,
texture_width,
texture_height,
projection_matrix,
is_camera_perspective,
viewport_width,
viewport_height,
camera_normal,
face_id
);

// Convert result to structured return type
emscripten::val result_polygons = emscripten::val::array();

for (size_t i = 0; i < result.size(); ++i)
{
emscripten::val polygon_array = emscripten::val::array();
const auto& polygon = result[i];
for (size_t j = 0; j < polygon.size(); ++j)
{
polygon_array.set(j, polygon[j]);
}

result_polygons.set(i, polygon_array);
}

return PolygonArray{ result_polygons };
}

EMSCRIPTEN_BINDINGS(uvula)
{
// Register TypeScript-style typed arrays
emscripten::register_type<Float32Array>("Float32Array");
emscripten::register_type<Uint32Array>("Uint32Array");
emscripten::register_type<Int32Array>("Int32Array");
emscripten::register_type<PolygonArray>("Point2F[][]");

// Register structured return types
value_object<UnwrapResult>("UnwrapResult")
.field("uvCoordinates", &UnwrapResult::uvCoordinates)
.field("textureWidth", &UnwrapResult::textureWidth)
.field("textureHeight", &UnwrapResult::textureHeight);

// Main typed functions with proper TypeScript signatures
function("unwrap", &unwrap);
function("project", &project);

function("uvula_info", &get_uvula_info);

class_<Geometry>("Geometry")
.constructor<const Float32Array&, const Uint32Array&, const Float32Array&, const Int32Array&>();

// Utility classes for direct access if needed
class_<Point2F>("Point2F")
.constructor<>()
.property("x", &Point2F::x)
.property("y", &Point2F::y);

class_<Point3F>("Point3F")
.constructor<float, float, float>()
.function("x", &Point3F::x)
.function("y", &Point3F::y)
.function("z", &Point3F::z);

class_<Vector3F>("Vector3F")
.constructor<float, float, float>()
.function("x", &Vector3F::x)
.function("y", &Vector3F::y)
.function("z", &Vector3F::z);

value_object<Face>("Face")
.field("i1", &Face::i1)
.field("i2", &Face::i2)
.field("i3", &Face::i3);

value_object<FaceSigned>("FaceSigned")
.field("i1", &FaceSigned::i1)
.field("i2", &FaceSigned::i2)
.field("i3", &FaceSigned::i3);
}
Loading
Loading