jscad / OpenJSCAD.org

JSCAD is an open source set of modular, browser and command line tools for creating parametric 2D and 3D designs with JavaScript code. It provides a quick, precise and reproducible method for generating 3D models, and is especially useful for 3D printing applications.
https://openjscad.xyz/
MIT License
2.63k stars 511 forks source link

"subtract" gives bad output. #1113

Open briansturgill opened 2 years ago

briansturgill commented 2 years ago

Expected Behavior

Actual Behavior

Meshlab reports 12 non manifold vertices 64 faces over non manifold edges. (I've seen this several times, here's a simple example.)

Steps to Reproduce the Problem

Run this: program output to .stl const { cylinder } = require('@jscad/modeling').primitives const { translate } = require('@jscad/modeling').transforms const { subtract, union, intersect } = require('@jscad/modeling').booleans

const main = () => { h = 8 size = 3 const inner_d = size*0.8 const outer_d = 4+size; c1 = cylinder({radius: outer_d/2, height: h}) c2 = cylinder({radius: inner_d/2, height: h}) let shape = union( subtract(c1, c2), //translate([20, 0, 0],union(c1, translate([3, 0, 0],c2))), //translate([0, 20, 0],intersect(c1, translate([3, 0, 0],c2))) ) return shape; }

module.exports = { main }

Run meshlab on the stl file.

Specifications

OpenJSCAD 2.2.21 [Linux:5.15.0-39-generic,linux:x64]

briansturgill commented 1 year ago

I'll probably do more of this discussion to Manifold's board, but felt this thread needs an update. @elalish where would you want this done, Discussions?

Using union script by @platypii (translated to python and adding subtract and intersect) I managed to get a first round of timing tests comparing the C# version of the BSP algorithm with Manifold's algorithm, both using their Python bindings.

I then spent the rest of the day trying to figure out if it was actually true. I've been through it thoroughly and it appears that Manifold's booleans are 3 ORDERS OF MAGNITUDE (1000x) faster than the BSP-based booleans in JSCAD/CSharpCAD.

There are some caveats.

First, the sphere code between the two was different, but I changed the segment values (100 for JSCAD/CSharpCAD and 150 for Manifold) so that the resulting sphere had approximately the same number of vertices and faces.

The Python interface to C# is probably less efficient than the one to Manifold, however the overhead is probably not substantial realtive to the operations being tested.

The test by @platypii is edge case oriented... I don't know if that is having an effect or not.

Finally, JSCAD/CSharpCAD is faster handling the "disjoint" case as we have special handling for that. I'm assuming Manifold doesn't bother checking (it's still very fast for that case).

Here is my code for Manifold:

from pymanifold import Manifold
from time import time_ns

segments = 150

def test(x, radius, name):
    s1 = Manifold.sphere(radius=1, circular_segments=segments)
    s2 = Manifold.sphere(radius=radius, circular_segments=segments).translate(x, 0, 0)
    start = time_ns()
    ret = s1+s2
    finish = time_ns()
    if name == "ignore":
        return
    print("union", name, (finish-start)/1000000.0)
    start = time_ns()
    ret = s1^s2
    finish = time_ns()
    print("intersect", name, (finish-start)/1000000.0)
    start = time_ns()
    ret = s1-s2
    finish = time_ns()
    print("subtract", name, (finish-start)/1000000.0)

test(4, 1, "ignore");
test(4, 1, "disjoint");
test(2, 1, "touching");
test(1, 1, "overlap");
test(0, 1, "same");
test(0, 0.5, "inside");

Using separate programs, I checked the output from Manifold and it appears correct. Indeed, in the case "union overlap", CSharpCAD was producing output that was not manifold. Manifold handled it correctly.

I'm hoping someone on the JSCAD team will attempt timing tests with the WASM version. I will be working on the WASM version (under C#'s new wasmtime package) soon.

hrgdavor commented 1 year ago

@briansturgill, @elalish that is great news, looking forward to trying out manifold wasm

elalish commented 1 year ago

Thank you @briansturgill! You really made my day with your analysis.

Manifold does in fact have a fast path for disjoint inputs, but it could probably use further optimization; I'd guess we're not skipping as much as we could.

elalish commented 1 year ago

Oh, forgot to answer the question: Yes, please start a Discussion thread in Manifold.

nmattia commented 5 months ago

I've also got issues with a subtract call (slicer mentions "open edges"), would this be the same issue? and if so, are there any workarounds currently available?

Here's a screenshot from Blender, the selected vertex can be moved freely away from the vertical face. Is that what is considered an "open edge"?

Screenshot 2024-03-17 at 14 35 57

Code below, without the final subtract code the slicer doesn't complain about any open edges

model.js ```js const { sphere, roundedRectangle, cube, line } = require('@jscad/modeling').primitives const { extrudeLinear, extrudeRectangular } = require('@jscad/modeling').extrusions const { translateZ } = require('@jscad/modeling').transforms const { subtract } = require('@jscad/modeling').booleans const main = () => { const mm = 1; const cm = 10; const widthOuter /* (X) */ = 20 * cm; const depthOuter /* (Y) */ = 10 * cm; const heightOuter /* (Z) */ = 10 * cm; const bottomThickness = 2 * mm; const wallsThickness = 4 * mm; const radius = 1 * cm; const baseshapeCenter = [0, depthOuter/2] const baseshape = roundedRectangle({ center: baseshapeCenter, size: [widthOuter, depthOuter], roundRadius: radius }); const ext = extrudeLinear({ height: heightOuter}, baseshape); // ext exports fine const radiusInner = radius - wallsThickness; const baseshapeNeg = roundedRectangle({ center: baseshapeCenter, size: [widthOuter - 2* wallsThickness, depthOuter - 2 * wallsThickness], roundRadius: radiusInner }); const extNeg_ = extrudeLinear({ height: heightOuter - bottomThickness}, baseshapeNeg); const extNeg = translateZ(bottomThickness, extNeg_); const res = subtract(ext, extNeg); return [res] } module.exports = { main } ```

Let me know how I can help, I'm keen to get this resolved!

edit:

The changes only occur when you output an STL file.

I might just try another export format then!

edit 2: nope, same thing with OBJ, 3MF, etc

hrgdavor commented 5 months ago

manifold library has come a long way from when this thread was started. I am following it closely, and finally have some basinc examples working in jscad.app. My guess with manifold you would not have those errors @nmattia.

I firmly believe future of jscad is in adopting manifold (if not completely, at least as an optional CSG backend)

nmattia commented 5 months ago

@hrgdavor thanks! I'm actually giving it a try. The API reference seems to have moved so it's a slow but promising start (the part that failed in JSCAD is exporting without issues in Manifold).

I did have a look at the JSCAD sources and I'm actually not sure it's a boolean issue afterall (in my case); looks like it might be an issue with the way JSCAD triangulates. The STL export does have some triangulation code but AFAICT it shouldn't actually run because the model goes through triangulatePolygons & friends first. Once I got there I decided to give Manifold a go 😬

Will report on how that goes!

z3dev commented 5 months ago

@udif JSCAD internally does care if polygons are triangulated or not. That's the biggest difference with modern CSG engines.

let us know if you find anything suspicious, and can reproduce with a fairly simple test.

z3dev commented 5 months ago

I've also got issues with a subtract call (slicer mentions "open edges"), would this be the same issue? and if so, are there any workarounds currently available?

Here's a screenshot from Blender, the selected vertex can be moved freely away from the vertical face. Is that what is considered an "open edge"?

Code below, without the final subtract code the slicer doesn't complain about any open edges

@nmattia can you supply the name of the slicer? There are many and many are subpar when dealing with mesh issues.

Blender also has 'repair' functions as Blender itself can produce invalid meshes.

My personal favorite is Meshlab. You might want to look at this application.

nmattia commented 5 months ago

@z3dev do you mean @nmattia ?

The slicer is PrusaSlicer, which does autorepair. Regardless of the slicer, the blender screenshot does show the issue. IIUC the technical term for the STL rule being broken here is "vertex-to-vertex rule":

Vertex-to-vertex rule. Each triangle must share two vertices with each of its adjacent triangles. In other words, a vertex of one triangle cannot lie on the side of another. fabbers.com, The Stl Format

Or do you mean that, upon import, Blender messed with the Mesh which makes the screenshot moot? I'll admit I didn't read the actual STL output... 😂

Thanks for pointing out Meshlab, I'll keep it in mind!

EDIT: just to clarify, PrusaSlicer & Blender both showed the same issue, or rather both showed an issue at the same spot. PrusaSlicer just highlighted the edges in red, and Blender showed the vertex was on the edge and not connected to another face

elalish commented 5 months ago

Coming in from the Manifold side of things... Please keep in mind that STL is a lossy format regarding manifoldness. Effectively all STLs (even ones produced from a proper manifold) require fixing and that fixing process is heuristic and can easily result in a previously manifold mesh becoming non-manifold, but worse, the results will be different when imported through different software. I would strongly recommend 3MF (or even OBJ) as alternatives that actually encode manifoldness (topological) data. I also authored an EXT_mesh_manifold extension to glTF that works properly even when you have discontinuous vertex properties on a mesh.

z3dev commented 5 months ago

The slicer is PrusaSlicer, which does autorepair. Regardless of the slicer, the blender screenshot does show the issue. IIUC the technical term for the STL rule being broken here is "vertex-to-vertex rule"

This the typical issue reported as booleans operations do not produce 'watertight' meshes. That's why all the generalize functions were created. The output is typically good enough for the slicers to print or to fix.

Or do you mean that, upon import, Blender messed with the Mesh which makes the screenshot moot?

Absolutely not. I was just wondering which slicer was being used. Not all slicers are the same, and Prusa is one of the better ones.