Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JS bindings for triangulation #393

Merged
merged 3 commits into from
Apr 4, 2023

Conversation

jirihon
Copy link
Contributor

@jirihon jirihon commented Mar 28, 2023

The triangulation algorithm available from JS allows you to create specific shapes efficiently, often without the need for boolean operations.

I was missing this very much as I frequently create custom polyhedrons, either from scratch, or based on modified geometry. This allows me to construct a manifold from a set of planar 3D polygons. I just project them to 2D (axis-aligned projection), triangulate and feed all the triangles to the manifold mesh constructor. Similar functionality is in OpenSCAD too (polyhedron).

I believe the triangulation could be useful for many other users of the JS bindings and hope you'll be willing to merge this in some form.

@hrgdavor
Copy link

hrgdavor commented Mar 28, 2023

I would definitely love to see this too :) . I also have a tendency to produce some shapes without booleans procedurally.

Copy link
Owner

@elalish elalish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good idea. I've basically been waiting for someone to ask for this, because I wasn't quite sure what the right API shape would be.

bindings/wasm/bindings.cpp Outdated Show resolved Hide resolved
bindings/wasm/bindings.cpp Outdated Show resolved Hide resolved
for (auto& poly : polygons) {
SimplePolygonIdx simpleIndexed;
for (const glm::vec2& polyVert : poly) {
simpleIndexed.push_back({polyVert, idx++});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies the output indices refer to the polygon points in order, which I suppose is simple enough. I guess you have to do a mapping back to your polyhedron verts then? It would be helpful to include an example showing how to use this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I do backmapping of the verts. What is the right place for such an example?

Copy link
Contributor Author

@jirihon jirihon Mar 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an example how I use the triangulation API:

// Inputs:
// vert_pos: set of 3D points
// polygons: set of polygons, wound CCW and representing multiple polygons
//           and/or holes, polygon point is expressed as an index to vert_pos
// normal: normal of the polygons plane

const projection = axis_aligned_projection(normal);
let idx = 0;
const vert_i_map: number[] = [];
const polygons_2d = polygons.map((polygon) =>
	polygon.map((vert_i) => {
		vert_i_map[idx++] = vert_i;
		return projection(vert_pos[vert_i]);
	}),
);
const result = manifold.triangulate(polygons_2d);
const tris = result.map((tri) => tri.map((vert_i) => vert_i_map[vert_i]));

function axis_aligned_projection(normal) {
	const abs_normal = new Vector3(Math.abs(normal.x), Math.abs(normal.y), Math.abs(normal.z));
	let x, y, z;

	if (abs_normal.z > abs_normal.x && abs_normal.z > abs_normal.y) {
		// x-y plane
		z = normal.z;
		x = 0;
		y = 1;
	} else if (abs_normal.y > abs_normal.x) {
		// z-x plane
		z = normal.y;
		x = 2;
		y = 0;
	} else {
		// y-z plane
		z = normal.x;
		x = 1;
		y = 2;
	}
	if (z < 0) {
		return (v) => [v[y], v[x]];
	} else {
		return (v) => [v[x], v[y]];
	}
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about creating a new sample next to knot.cpp - Write in some reasonably simple polyhedron, then run your code to turn it into a Manifold. Then you can add it to samples_test and check its volume, which should give us decent test coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble coming up with a good example. If I make something simple, e.g. a modified cube with the top face a little bit smaller than the bottom face and with a square hole through both top and bottom face, it feels too simple to be included in the samples. Right now, samples are pretty complex awesome artistic objects.

On the other hand, if I make something more sophisticated like a basic chair (without the need for booleans), it would be more practical to have some kind of general polyhedron constructor function than just a triangulation function. And for this polyhedron constructor, TriangulateIdx is more practical than Triangulate.

So after thinking about this more deeply I suggest two things:

  1. Add a polyhedron constructor. Possibly in constructors.cpp? This could be also exposed in the JS bindings. There is, however, one limitation of the following implementation. The face triangles can't be backmapped to the input face polygons as we don't know how many triangles make up which face. We could easily return tri2face mapping from this function alongside the Manifold object (which would make it an outlier compared to the other constructors always returning just a manifold), or leave the users to use the triangulation directly if they need the mapping. I personally prefer the direct use of triangulation in this case as I'm able to prepare all the triangles together with normals and UVs in one go.
Manifold Manifold::Polyhedron(
    const std::vector<glm::vec3> vertPos,
    const std::vector<std::vector<std::vector<int>>>& polygonsVec,
    const std::vector<glm::vec3> normals) {
  Mesh mesh;
  mesh.vertPos = vertPos;

  for (int i = 0; i < polygonsVec.size(); i++) {
    auto& polygons = polygonsVec[i];
    std::vector<glm::ivec3> tris;

    if (polygons.size() == 1 && polygons[0].size() == 3) {
      auto& p = polygons[0];
      tris.push_back({p[0], p[1], p[2]});
    } else if (polygons.size() == 1 && polygons[0].size() == 4) {
      auto& p = polygons[0];
      tris.push_back({p[0], p[1], p[3]});
      tris.push_back({p[1], p[2], p[3]});
    } else {
      auto& normal = normals[i];
      const glm::mat3x2 projection = GetAxisAlignedProjection(normal);
      auto polygons2d = To2DPolygon(polygons, projection, vertPos);
      tris = Triangulate(polygons2d);
    }
    for (auto& tri : tris) {
      mesh.triVerts.push_back(tri);
    }
  }
  return Manifold(mesh);
}
  1. Add TriangulateIdx to the JS bindings, either as a replacement of Triangulate, or as an extra option. This would make it easier to implement custom polyhedron constructors in JS as it would not need remapping. The typescript signature could follow the C++ signature.
export type PolyVert = [Vec2, number]
export type SimplePolygonIdx = PolyVert[];
export type PolygonsIdx = SimplePolygonIdx[];
export function triangulateIdx(polygons: PolygonsIdx, precision?: number): Vec3[];

I look forward to hearing what you think about this. I'm sorry that I can't react faster, but the timezone shift makes it hard 🙂

Copy link

@hrgdavor hrgdavor Mar 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it looks like adding polygon triangualtion utility into manifold would make sense if a big performance boost can be achieved. How about only for 2d that is extruded ? ... I am following discussions, but have not found time to go further than git pull and running existing examples, so my comments may be out of place from lack of insight.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have Manifold::Extrude() already, so that should be covered.

Copy link
Contributor Author

@jirihon jirihon Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit, I got lost in the discussion 😄 I'll try to add something reasonable.

From my perspective, polyhedron constructor is just a convenient way to make triangle meshes. I don't expect any special validity checks to happen on the input - it is my problem if I have it wrong.

In my project, I do a lot of derived geometry. Let's say I have a polyhedron as input and I need to push one of its faces. Or I want to "extrude" one of its faces. Or both at the same time. See the pictures below.

The first picture shows the input polyhedron. The front face is selected, so you can see what triangles it consists of.

Snímek obrazovky z 2023-04-04 13-38-43

The second picture shows the output after "extruding" the blue object from one of the other faces. The geometry of the blue object is calculated by moving the face points and using custom JS implementation of polyhedron constructor which uses manifold's triangulation. The input polyhedron is also modified. The affected points are shifted and the faces are re-triangulated. In this case, the re-triangulation is really needed, the input triangles would not work for the changed front face.

Snímek obrazovky z 2023-04-04 13-38-36

You may wonder why I need manifold at all as this could be done with triangulation only. But I also need fast booleans, mainly for cutting the objects into pieces.

So this is my use case for working with polyhedrons (as an abstraction over triangle mesh) and I accept the risk of rounding errors. After all, who can find a perfectly planar polygon in the real world?

But now back to the original topic:

I think it is enough for manifold to give access to the triangulation 📐 So it is ok to merge this PR without further additions 😄

I just can't think of any example showcasing the triangulation that wouldn't be trivial or too tedious to make. Please, do you have any ideas? And regarding tests, I think the triangulation is well tested right now.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation, I agree this PR can merge as is. That's great that you're working on face push/pull - honestly I was thinking this could be a good feature for Manifold. Internally we already have a concept of polygon faces: groups of neighboring triangles with the same triRef.tri. Would you have any interest in contributing this feature into Manifold?

Copy link
Contributor Author

@jirihon jirihon Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for merging! Regarding the push/pull, I don't feel so confident in C++ to implement it. I feel more at home in JS/TypeScript world.

And I'm aware of the triRef.tri and rely on that heavily when extracting face polygons. It is extremely valuable for me. It allows me to reference faces in a stable way across multiple operations with the manifold.

rename original Triangulate to TriangulateIdx,
move TriangulateJS from bindings.cpp to polygon.cpp,
rename TrinagulateJS to Triangulate for consistent C++ and JS API,
add optional precision parameter
@codecov
Copy link

codecov bot commented Mar 29, 2023

Codecov Report

Patch coverage: 30.00% and project coverage change: -0.14 ⚠️

Comparison is base (07513bd) 86.04% compared to head (bc72cbf) 85.90%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #393      +/-   ##
==========================================
- Coverage   86.04%   85.90%   -0.14%     
==========================================
  Files          36       36              
  Lines        4485     4492       +7     
==========================================
  Hits         3859     3859              
- Misses        626      633       +7     
Impacted Files Coverage Δ
src/polygon/src/polygon.cpp 84.47% <12.50%> (-1.34%) ⬇️
src/manifold/src/constructors.cpp 94.70% <100.00%> (ø)
src/manifold/src/face_op.cpp 98.55% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@jirihon jirihon requested a review from elalish March 29, 2023 08:08
@elalish
Copy link
Owner

elalish commented Mar 30, 2023

This looks ready to merge to me; let's just get the sample in there so people know how to use it.

@pca006132
Copy link
Collaborator

And maybe add a few simple tests as well? So we can check if it breaks later.

Copy link
Owner

@elalish elalish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@elalish elalish merged commit fbe4cc6 into elalish:master Apr 4, 2023
@elalish elalish mentioned this pull request Apr 11, 2023
@elalish elalish mentioned this pull request Nov 3, 2023
cartesian-theatrics pushed a commit to SovereignShop/manifold that referenced this pull request Mar 11, 2024
* js binding for triangulation

* don't use CrossSection class as it alters the order of the vertices

* remove unused code,
rename original Triangulate to TriangulateIdx,
move TriangulateJS from bindings.cpp to polygon.cpp,
rename TrinagulateJS to Triangulate for consistent C++ and JS API,
add optional precision parameter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants