SolidCode / SolidPython

A python frontend for solid modelling that compiles to OpenSCAD
1.1k stars 172 forks source link

Possibility of supporting ImplicitCAD (extopenscad)? #134

Open Digital-Monk opened 4 years ago

Digital-Monk commented 4 years ago

ImplicitCAD doesn't yet have everything that OpenSCAD has. But it does have at least one killer feature that OpenSCAD lacks: trivial fillets for cubes and boolean operations.

https://github.com/colah/ImplicitCAD

(It has several other nice features -- but fillets are the one single thing that would force me to leave OpenSCAD and use FreeCAD or something else that "gets" fillets. So it makes me happy to have them in an "extended OpenSCAD" compiler.)

Instead of cube([10,10,10]);, if you say cube([10,10,10],false,1);, you'll get a 10 unit cube with 1 unit fillets along all edges. (The 'false' is a value for 'centered'. My quick and dirty experiments only worked if I used positional arguments in that order, but there may be better ways to do this).

difference(), union(), and intersection() now all take an 'r' parameter that specifies the radius of the fillet to apply along the edges.

It's fairly trivial to load the result of scad_render_to_file(object) into a text editor and just paste the necessary extra parameters into place, but it would be slick if there was some way to do this directly from SolidPython.

Not sure of the best approach, but probably one settable flag to enable .escad output, and then a way to specify a fillet radius. My lazy feeling is to just make it a setting in the solid library, so that I can tell solid to use 1 unit fillets and then go on about my business of creating geometry, knowing that it will apply a 1 unit r to any cubes or operations that it generates. In my head, this seems reasonable, because you probably want the same fillet for each conceptual chunk of your model, so set it once, build, then change it before building a different piece.

You could allow the fillet radius in the cube function easily enough. And you could likewise accept and pass through the fillet in the union/difference/intersection functions. But it's kind of slick to be able to use +, -, and * instead, and there's no straightforward way to give them that extra parameter...

Alternatively, maybe there could be a way to specify a fillet radius for a SCAD object. That seems more complicated to me, because then you have to answer some design questions:

  1. If the SCAD object is the result of operations on other objects, should you recursively apply the new fillet down the tree? That would override any lower level fillets already in place...
  2. When performing an operation on SCAD objects, should the result inherit fillets from the children? This raises questions if the input objects have different fillets...
  3. If you neither recurse nor inherit, then you have to spend a lot of time setting fillets on each piecewise operation result, which seems tedious...

Sorry if this is rambling or disorganized. I found ImplicitCAD and SolidPython essentially on the same day after years of loving OpenSCAD's programmability and hating how hard it made some things. I may have gotten a bit over-excited ;)

etjones commented 4 years ago

I've been following Chris Olah's work for a long time now. Dudes' straight-up brilliant. ImplicitCAD's integral rounding is pretty sweet.

I've been revisiting fillets & 2D offsetting in SP in the last couple weeks. I don't know how much you've looked at the OpenSCAD's offset() function, but it's surprisingly capable, albeit limited to 2D fillets/rounds. See here for some capable helper functions for dealing with the fine points of offset() arguments.

That doesn't really get you to the impressive state of ImplicitCAD's 3D rounding, though. Because SP is just a translation layer from Python to OpenSCAD, it's difficult to just add functionality to the base functions likecube() or union(). Not impossible, but I'm not super inclined to go replacing calls to cube(side, r=fillet_rad) with, for example, minkowski(fillet_rad){ cube(side-2*fillet_rad)}; That would yield a rounded cube, but at the expense of straying a little far from the base OpenSCAD language.

A couple directions I could see this going are:

So that's a long way of saying, no, SolidPython probably won't have explicit support for ImplicitCAD; it's different enough that we'd have to make some deep changes and I think it's dangerous for a project to try to serve two masters like that. I'd be happy to see a PyImplicitCAD fork of SP, though. I don't think it would take too much work, and could open up ImplicitCAD's potential use cases a lot farther. If that's something you want to take on, I'd be happy to talk you through any of the less-clear bits of SolidPython's rendering codebase.

etjones commented 4 years ago

Hmm. On thinking about this a little more, it would actually be a pretty simple (although touching a lot of classes) change to allow all SP classes (cube(), etc) to take extra keyword args. An implicit_scad_render() function would then output those extra args, while scad_render() would ignore them. That feels a little hacky, since all classes would now accept arbitrary arguments, but it's not too far out of reach. I'd be willing to talk through a PR for that.

You also might be able to write a monkey-patching system, add_implicitCAD_args() that would alter all currently existing classes to accept implicitCAD's arguments, and render those classes accordingly. I kind of prefer that approach, since all ImplicitCAD-related code could be kept in one file solid.implicit and we wouldn't have to make changes to existing OpenSCAD-based code. I'd definitely be OK with a single-file, monkey-patched, ImplicitCAD compatibility system. Any interest?

Digital-Monk commented 4 years ago

Sounds entertaining. Never monkey patched before, but quick search makes it look straightforward enough. As a starting point (80% solution), I would focus on cube, union, difference, and intersection. Haven't looked at the SolidPython code yet, but is that 4 classes, or noticeably more complicated than that? Guess I should just clone the repo and look :)

Glancing and thinking out loud:

nickc92 commented 4 years ago

Just thought I'd mention that you can get a cube with rounded edges using the Minkowski sum with a sphere; in solidpython, e.g.:

def fillet_cube(lens, fillet_rad):
    return minkowski() (cube([l - 2 * fillet_rad for l in lens]), sphere(fillet_rad))

That being said, ImplicitCAD does look intriguing, and I'd consider contributing to implicit_scad_render(). I'm curious to see if it renders STLs much faster (it claims that it can, at the expense of accuracy). I wrote the viewscad package, which lets you do SolidPython designing within a jupyter notebook, and produces interactive 3D renderings within the notebook. I use it for my own CAD stuff. The main problem with it is that in order to render something, it calls OpenSCAD to fully render an STL, which can take some time if it's an object of any complication. If ImplicitCAD can do this rendering much faster, it might make for a much better experience in viewscad.

Digital-Monk commented 4 years ago

STL generation speed is dependent on the resolution you request. Basically, ImplicitCAD is building a "field strength" model, and then it uses some variant of marching cubes to tile triangles at the "field strength == 0" surface. (Oversimplified, and probably wrong in critical details, but close enough to distinguish it from OpenSCAD's approach)

Building up the internal model seems to be ridiculously fast. Marching the cubes over it can be fast, or can be painful... It's a little like setting $fn too high, except that $fn only affects curved surfaces, but setting ImplicitCAD's output resolution too high affects everything. Still, pretty easy to have a couple of resolution settings for preview and final. And the compiler will pick a resolution on its own that lets it do a pretty-fast-but-not-very-clean model.

(I love minkowski, but I hate the render times...)

etjones commented 4 years ago

So... I got to thinking about this one and decided to just go ahead and implement it. I've pushed a trial to a branch, and the monkey patch code is here. Basically, for any of several SolidPython classes (cube, union, etc.), I subclass the original classes and add a r argument. Usage is:

from solid.implicit import *   #( Or from solid import cube, union... if you want to be precise)
c = cube(2, r=1)
implicitcad_render_to_file(c) # outputs a file with '.escad' suffix by default

I'm going to leave this issue open for now. Ideas or edits welcome!

etjones commented 4 years ago

Just tried this out on ImplicitCad.org's web editor. Here's a SP version of the first rounded ImplicitCad shape in their docs

from solid.implicit import *  
a = union(r=14)( 
    square(80),  
    translate([80,80])(
        circle(30)
    )
)  
print(implicitcad_render(a))   

Yields the ImplicitCad code:

union(r = 14) {
    square(r = 0, size = 80);
    translate(v = [80, 80]) {
        circle(r = 30);
    }
}

This renders correctly on http://www.implicitcad.org/, but generates a bunch of errors; looks like this feature might require some more massaging to keep those APIs synced up.

Error samples from code above:

Error at line 5, column 9: no instance of square found to match given parameters.
Instances available: Module square[ (x=..., y=..., center) {}, (size=..., center) {} ] Error at line 5, column 9: Insufficient parameters. Passed 2 named parameters. Couldn't match 2 parameters: "x" and "y". Had 2 extra parameters. They are:"r" and "size". Error at line 5, column 9: Too many parameters: 2 extra. Passed 2 named parameters. Couldn't match 2 parameters: "x" and "y". Had 2 extra parameters. They are:"r" and "size". Error at line 5, column 9: Too many parameters: 1 extra. Passed 2 named parameters. Had one extra parameter: "r"
Digital-Monk commented 4 years ago

solid.implicit seems perfectly reasonable to me, because the cabal package (like pip but for haskell) is just called implicit.

I've never really done animation in OpenSCAD/ImplicitCAD, so I'm probably not going to be much use there, even for ideas. As a ludicrously cheesy pass-through solution, could your implicit rendering code check the type of the values in the object, and if it's a string, just place that string directly in the .escad code? Would allow passing functions into ImplicitCAD, but it's probably too horrible to consider...

My personal experience with parameters to ImplicitCAD suggested that it doesn't really understand named parameters. At least, the only way I was able to get stuff to work without errors was to use positional arguments. So, the cube() call (at least for me) had to be cube([x,y,z], centered, radius). I haven't played with the 2D stuff much, so I don't know if it's any better.

Hmmm... Played with that example some more, and it appears that ImplicitCAD does understand named parameters and can handle them being "out of order", but there must be some kind of pre-parser that is out of sync with the actual logic, and uses different rules to validate the syntax. So you get syntax "errors", and then it goes ahead and does the right thing anyway... For now, it looks like the quick patch got the job done, and the extraneous errors need to be fixed on the ImplicitCAD side.

Digital-Monk commented 4 years ago

So, turns out that the "stable" build of ImplicitCAD may be almost 2 years old, so the warnings from the online interpreter might just be stale artifacts. Sounds like you've got a good enough implementation for now. Thanks!

etjones commented 4 years ago

Nice! I think it wouldn't be too hard to do a custom version of scad_render() that removed argument names, for instance, if we needed to. I'll keep this issue open for a few days in case anyone playing with the branch wants to look through things (@nickc92 ? might be a speedup for viewscad), and then I'll merge to master if nothing else pops up. Cheers!

etjones commented 4 years ago

Hi guys - I just looked over this some more. There's some more work -- unit tests, documentation, README-- that I want to add before merging to master. I've got some things going on right now (baby on the way, yay!) that mean I may not get to that maintenance work right away, but I'll leave this issue open for the moment with a pointer to the branch for anybody who stumbles on it. I'll update when I've cleaned up all the plumbing work.

jeff-dh commented 3 years ago

The mentioned branch above is based ontop of the use_py_scadparser branch and basicly uses the import_scad feature to import the OpenSCAD builtins ("commands" like square, sphere, translate, union, difference, intersection, hull,....) from a *.scad file (which is actually name builtins.openscad).

This allows it to easily replace the complete set of builtins (e.g. OpenSCAD builtins / parameters) with a different set of builtins (e.g. ImplicitCAD builtins / parameters) by just editing or adding an OpenSCAD file which only includes module definitions and looks like this:

module union();
module difference();
module intersection();
module intersection_for(n);

module translate(v);
module scale(v);
module rotate(a=default, v=default);
module mirror(v);
module resize(newsize, auto=default);

module color(c, alpha=1.0);

module minkowski();
module offset(r=default, delta=default, chamfer=default, segments=default);
module hull();
module render(convexity=default);

module linear_extrude(height=default,
                      center=default,
                      convexity=default,
                      twist=default,
                      slices=default,
                      scale=default);
......

If I understand the "issue(s) with ImplicitCAD" right (basically a different set of parameters to the builtins?), this could be a pretty nice way to get there by changing a config file (and reducing the code size). One would only need to create an interface to switch between builtins.openscad and builtins.implicitCAD config file.

This is not tested a lot! These are all more drafts and proof of concepts than ready to ship code.

jeff-dh commented 3 years ago

implicit_car

ExpSolid ImplicitCAD example

Works with ImplicitCAD 0.3.0.1 and the online renderer without any issues.

BUT this creates limitations: you can't use a lot of libraries (mcad, bosl, bosl2 they all seem to be not ImplicitCAD compatible). You can only use the implicit builtins / api.

jeff-dh commented 3 years ago

implicit_parameter_functions

[...]
def ex2():
    twist_func = scad_inline_parameter_func("twist(h) = 90*cos(h*2*pi/40)")

    return linear_extrude (height = 40, twist = twist_func) (
               difference () (
                   shell(2) (circle (10)),
                   square(x=[0,20], y=[-4,4]),
               )
           ).rotate(0, 0, 90).left(50)
[...]

https://github.com/jeff-dh/SolidPython/blob/exp_solid/solid/examples/15-implicitCAD2.py