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

Boolean Path/Region Functions #3288

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

revarbat
Copy link

This came up on the mailing list a few weeks ago, and it seemed numerous people wanted it, so I've implemented it. These are very slow operations to implement in userspace. I know, because I did. They can be quite useful for many things, including skin() operations. The offset_paths() operation is especially useful. We've been using userspace versions in our library BOSL2 for a number of complex operations, and the userspace version is slooooooooooooow. OpenSCAD has the ClipperLib linked. It only makes sense to let users use that.

I've added the following 2D boolean geometry path/region functions:

  • union_paths(pathlist, pathlist,...); or union_paths(pathlists);. Returns a pathlist.
  • difference_paths(pathlist, pathlist,...); or difference_paths(pathlists);. Returns a pathlist.
  • intersect_paths(pathlist, pathlist,...); or paths_paths(pathlists);. Returns a pathlist.
  • minkowski_paths(pathlist, pathlist,...); or minkowski_paths(pathlists);. Returns a pathlist.
  • offset_paths(pathlist, [r], [delta], (chamfer]). Returns a pathlist.
  • point_in_paths(pathlist,point). Returns -1 if point is on a path, 0 if outside, and 1 if inside.

A 2D path, AKA a 2D polygon is a list of 2D points.
A pathlist, AKA a region, is a list of 2D paths.
A pathlists argument is a list of pathlists or paths.
All functions taking one or more pathlist arguments can accept a path instead.

…_paths(), offset_paths(), and point_in_paths() builtin functions for 2D boolean path/region manipulation.
@rcolyer
Copy link
Member

rcolyer commented Sep 27, 2020

@revarbat It looks like the sort of thing that is potentially very useful, and doing useful things faster is great, but currently I don't understand the exact scope of its usage. Could you provide some specific use case examples here for each of the new features that highlights the intended use and the value of each one? That would help us to evaluate it better.

Essentially, it's 5 new specific commands in a language that keeps very few for top-level simplicity, so we need to make sure we understand the applicability, scope, and value before integrating them.

@revarbat
Copy link
Author

Some definitions, first:

  • A 2D path is an ordered list of 2D point vectors that represents the outline of a polygon. In fact, you can pass a path to polygon() to render it into geometry.
  • A region is a list of one or more closed 2D paths that are all XORed from each other. If one path is fully inside another, it is subtracted from it. If two paths overlap, the common area is subtracted away from the non-overlapping parts.

The basic idea of this PR, is to provide access to the fast 2D boolean geometry functions, for manipulating paths and/or regions.

  • union_paths() lets you do for 2D paths or regions what union() {...} does for 2D geometry.
  • difference_paths() lets you do for 2D paths or regions what difference() {...} does for 2D geometry.
  • intersect_paths() lets you do for 2D paths or regions what intersection() {...} does for 2D geometry.
  • offset_paths() lets you do for 2D paths or regions what offset() {...} does for 2D geometry.
  • minkowski_paths() lets you do for 2D paths or regions what minkowski() {...} does for 2D geometry.

These all can interchangeably take paths or regions as arguments, and they all return regions. These regions can be manipulated in powerful ways that OpenSCAD does not otherwise provide, such as passing them to common sweep() or skin() functions from various libraries.

My BOSL2 library has nice documentation for functions in user space almost identical to these, though they are named union(), difference(), intersection(), etc without the _paths suffix:

For all of the examples below, presume there exist the following functions:

  • square() which takes the same arguments as the built-in 2D geometry module. This function returns a list of 2D points, in clockwise order from the X+ axis, that could be passed to polygon() to create the same square shape.
  • circle() which takes the same arguments as the built-in 2D geometry module. This function returns a list of 2D points, in clockwise order from the X+ axis, that could be passed to polygon() to create the same circle shape.

Also presume the existence of the following modules:

  • region(pathlist) which takes a path or list of paths, and creates a 2D geometry shape from it, by XORing all the paths in the pathlist.
  • sweep(profile, path3d) which takes a profile path or list of paths, and sweeps it along the given 3D path path3d.
  • skin(profiles) module that takes a list of 2D paths or regions/pathlists projected into 3D space, and skins a surface between them, including end-caps.
my_region = union_paths(
    square([40,100], center=true),
    square([100,40], center=true)
);
region(my_region);

or

my_region = union_paths([
    square([40,100], center=true),
    square([100,40], center=true)
]);
region(my_region);

Would generate the same 2D geometry as:

union() {
    square([40,100], center=true);
    square([100,40], center=true);
}

Similarly,

my_region = difference_paths(
    square(100, center=true),
    circle(d=40)
);
region(my_region);

or

my_region = difference_paths([
    square(100, center=true),
    circle(d=40)
]);
region(my_region);

Would generate the same 2D geometry as:

difference() {
    square(100, center=true);
    circle(d=40);
}

The intersect_paths() and minkowski_paths() functions similarly use the exact same syntax, to get the effects of intersection() {...} and minkowski() {...} respectively.

You can, of course, pass the output of one to the input of another:

my_region = union_paths(
    difference_paths(
        offset_paths(r=10, square(90, center=true)),
        circle(d=50)
    ),
    square([100,20], center=true)
);
region(my_region);

The use of point_in_paths() is mostly to provide a fast is-this-inside test for a region:

for (x = [-50:5:50], y=[-50:5:50])
    if (point_in_paths(my_region, [x,y])
        translate([x,y]) sphere(d=1, $fn=12);

@jordanbrown0
Copy link
Contributor

jordanbrown0 commented Sep 29, 2020

I would figure out how 3D shapes would be represented in data, to confirm that they could be unambiguously distinguished from 2D shapes (e.g. by the fact that they have 3 coordinates instead of 2), and then use the obvious union() et cetera names, without the _path suffix. (Especially since these operate on regions too.)

I'm not suggesting that you implement 3D variants of these, just that you ensure that they could be implemented.

For instance... extending your "paths" and "regions" concepts to 3D, you'd have polyhedra and lists of polyhedra. Extending your "a path could be passed to polygon()" model suggests that a polyhedron could be passed to polyhedron(), which in turn suggests that it's represented as

  • Vector of // really a structure, but we don't have those
    • Vector of points
      • Vector of 3 individual coordinates
        • number
    • Vector of faces
      • Vector of point indices
        • integer

and the 3D equivalent of a region would be a vector of those.

For reference, your paths are

  • Vector of points
    • Vector of 3 individual coordinates
      • number

and your regions are a vector of those.

Now, could a function like union() tell whether it's being passed a 2D object or a 3D object?

I was hoping that you could distinguish the four cases just by the structure of the vectors, like you can distinguish paths from regions because a path is a vector of vectors of numbers and a region is a vector of vectors of vectors of numbers. Alas, no, a polyhedron is a vector of vectors of vectors of numbers, and so is a region.

However, the dimensionality does save you: for a polyhedron [0][0] (the first point) has three elements, while for a region it has two.

So I think you can distinguish the two cases, and so naming these functions union() et cetera would not preclude adding 3D variants of them in the future.

But if one was really concerned, one could name these union2() et cetera, and name the future 3D variants union3() et cetera.

@revarbat
Copy link
Author

To be more specific, a 2D Path is a list of two or more 2D coordinate vectors like:

path = [ [10,-10], [-10,-10], [-10,10], [10,10] ];

And a 3D path is similar, with 3D coordinate points:

path = [ [10,-10,0], [-10,-10,10], [-10,10,20], [10,10,10] ];

A region is a list of zero or more paths:

region = [
    [ [10,-10], [-10,-10], [-10,10], [10,10] ], // path1
    [ [5,0], [0,-5], [-5,0], [0,5] ] // path2
];

In the BOSL2 library, the 3D equivalent of a 2D Region structure is called a VNF structure. (Short for Vertices 'N' Faces). It's a two-item list, where the first item is a list of vertices, and the second item is a list of faces, where each face is represented by a list of indices into the vertex list. ie:

vnf = [
    // Vertices
    [ [10,-10,-10], [-10,-10,-10], [-10,10,-10], [10,10,-10], [10,-10,10], [-10,-10,10], [-10,10,10], [10,10,10] ],
    // Faces
    [ [3,2,1,0], [0,1,5], [0,5,4], /* ... etc ... */ ]
];

You can pass a VNF to polyhedron() easily enough, like polyhedron(vnf[0], vnf[1]);.

Vectors, Paths, Regions, and VNFs are all easily distinguishable from each other:

  • Vectors are lists of numbers.
  • 2D Paths are a list of length 2 vectors.
  • 3D Paths are a list of length 3 vectors.
  • Regions are lists of lists of length 2 vectors.
  • VNF's are length 2 lists, where the first item is a list of length 3 vectors, and the second item is a list of integer vectors of length 3 or greater.

@revarbat
Copy link
Author

I can easily rename all the functions to remove the _paths suffixes. I just didn't want to assume that it was okay to abuse the separate namespaces for modules and functions. (Though I do that all the time in BOSL2)

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.

None yet

3 participants