SolidCode / SolidPython

A python frontend for solid modelling that compiles to OpenSCAD
1.11k stars 173 forks source link

new release has customiser UI #61

Open Neon22 opened 7 years ago

Neon22 commented 7 years ago

The customiser-style ui is now integrated into openSCAD. So there is apane with variables that the user can fool with interactively. IWBNI SolidPython could support this.

What this would mean (I think):

Maybe thereis a way to tag a parameter (using @ decorator maybe?) as a group name, or interactive variable...

etjones commented 7 years ago

Excellent! I haven't looked into this yet, but I've been wanting some better interactivity for a long time. I'll see what I can figure out there, and if you've got any code suggestions, I'd love to hear them. Cheers!

etjones commented 7 years ago

For future reference: docs for customizer syntax here: https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/WIP#Customizer

etjones commented 7 years ago

Looks like this shouldn't be too hard to do. My initial thought is something like:

from solid import *

cube_width = ScadSlider(default=5, min=1, max=20, description="Cube Width Label")

c = cube(size=[cube_width, 5, 5,])

print(scad_render(c))

# yields:
'''
// Cube Width Label
cube_width = 5; // [1:20]

cube(size=[cube_width, 5, 5]);
'''

The following classes should cover what's in the current docs:


ScadComboBox( list_or_dict)
ScadSlider()
ScadCheckbox()
ScadSpinbox()
ScadTextBox()
ScadVector()

Not quite sure how to handle code that involves actions on these adjustable parameters, like:

cube_width = ScadSlider(default=5, min=1, max=20)
some_string = ScadTextBox('default value')

# How to deal with arithmetic on numeric values?
cube(size=[2*cube_width, cube_width/other_param, 5])

# Or transforms on strings or vectors?
text(text=some_string + 's')

Subclassing numbers.Number or String seems like overkill, and trying to parse source for strings we can use in an expression seems problematic. The easy way out might be to only allow un-altered Customizer objects as arguments (i.e. cube(size=cube_width) would be OK, but cube(size=2*cube_width) wouldn't be), but that seems tacky to me. Would welcome other approaches to this.

Neon22 commented 7 years ago

How about - create a new special_named variable if there was any complex usage of a variable - then use that new name instead.

e.g. in above example - new variable UI_cube_width = 2*cube_width and cube(size=UI_cube_width) but this involves lexical analysis.

There is a python program that does this called rope which can do code refactoring automagically. If you did the refactoring in the python code before passing it to SolidPython then it might be quite simple to author. doc link below

Check out a related github project I started here for micropython (unfinished and languishing I admit) https://github.com/Neon22/python-flavin It uses this approach to search all imports to find every function,etc that is actually used and copy them all into a new import so that only the code and all children that is actually referenced, is compiled into python. using:

aarchiba commented 7 years ago

screenshot from 2017-05-30 23-38-00

Some things just aren't possible - if n_sides is a customizer value, then there's no way to (say) let python loop up to n_sides without rerunning python every time the customizer is tweaked. Since SolidPython doesn't generate loops (it doesn't normally need to) this makes customizer variables fundamentally less useful in SolidPython. Nevertheless, basic support could be had by allowing OpenSCAD expressions to be formed: say a CustomizerExpression class so that doing 2 x generates the OpenSCAD expression "2 (whatever is in x)".

Fundamentally interactivity is going to require rerunning python code, so it's in the domain of making python code interactive. I know IPython notebooks provide some features to allow web controls in cells to set variables for python. With suitable hackery these could be connected up to generate OpenSCAD scripts, and the autoreload could handle updating the rendered object, Edited to add: see attached screenshot for a simple demo.

etjones commented 7 years ago

I don't see any particular problem with running the python over and over and over again. Because SP is just doing some fancy string manipulation, it's rare for even big complex models to take more than a half- second or so to run. Not to say that it wouldn't be nice if we could avoid doing that, but changes like that would require a significant architectural change that's not entirely clear to me.

I think your IPython notebook example is a great way to go, @aarchiba; it would be cross-platform and wouldn't require much in the way of widget programming, which is appealing. Probably not going to get to that in the next couple months, but I think it would be a great addition to the project and I hope to have some time to spend on it in the autumn if nobody's beaten me to it

jeff-dh commented 3 years ago

I would suggest the following:

#test.py
from solid import *

outer_radius = 5
inner_radius = 2
thickness = 3

def customized_washer():

    washer = cylinder(outer_radius, thickness) - cylinder(inner_radius, thickness + 1).down(0.5)

    return washer
#some customizer interface (-> python shell for now ;)
>>> import test
>>> test.outer_radius=20
>>> test.inner_radius=16
>>> test.thickness = 4
>>> test.customized_washer()
difference() {
        cylinder(h = 4, r = 20);
        translate(v = [0, 0, -0.5000000000]) {
                cylinder(h = 5, r = 16);
        }
}
>>> test.customized_washer().save_as_scad() #updates OpenSCAD preview
'/home/xxx/xxx/SolidPython/solid/examples/customize/expsolid_out.scad'
>>> test.outer_radius=8
>>> test.inner_radius=4
>>> test.thickness = 2
>>> test.customized_washer()
difference() {
        cylinder(h = 2, r = 8);
        translate(v = [0, 0, -0.5000000000]) {
                cylinder(h = 3, r = 4);
        }
}
>>> test.customized_washer().save_as_scad() #updates OpenSCAD preview
'/home/xxx/xxx/SolidPython/solid/examples/customize/expsolid_out.scad'
>>> 

(This was tested with expsolid (https://github.com/jeff-dh/SolidPython/tree/exp_solid) but I assume it should work the same way with the regular SolidPython)

This is the command line version of a customizer. If you want a GUI, you could implement a solid-customizer-gui that might use inspect and do exactly what I did from the shell in a generic pattern. If you want sliders and stuff like that you could probably do it somehow with "annotations" or wrapping functions:

outer_radius = 5 #slider[1, 20,1]
#or
outer_radius = customizerGUI.getSliderValue("outer_radius", min=1, max=20, step=1)

or similar, I think this should be possible to implement.

As far as I understand the whole system I don't think it's possible to interact between OpenSCAD and (Solid)Python on a code-level (sharing variables). Therefor I see OpenSCAD only as a "backend" for SolidPython which could actually be replaced (by ImplicitCAD or any csg library). As such I think if SolidPython wants to provide features like a customizer or animated scenes it would have to implement them on it's own and keep using OpenSCAD as backend for processing the resulting csg tree (actually that's how SolidPython uses OpenSCAD and render_animate does this too).

I guess(!) -- any way of trying to overcome this barrier will become a hassle and will not be scalable properly. You will for example have a lot of restrictions. With this solution you can do anything, you can pass the customized variables into any python functions and do whatever you.

This solution is perfectly scalable to really large projects. You could have a config.py which configures a whole printer design (that's how Joseph Prusa did it for the I3, c.f. https://github.com/josefprusa/Prusa3/blob/master/box_frame/configuration.scad.dist).

#config.py
outer_radius = 5
inner_radius = 2
thickness = 3
#test.py
from solid import *
import config

def customized_washer():

    washer = cylinder(config.outer_radius, config.thickness) - cylinder(config.inner_radius, config.thickness + 1).down(0.5)

    return washer
#some customizer interface (-> python shell for now ;)
>>> import config
>>> import test
>>> config.outer_radius = 10
>>> config.inner_radius = 8
>>> config.thickness = 3
>>> test.customized_washer()
difference() {
        cylinder(h = 3, r = 10);
        translate(v = [0, 0, -0.5000000000]) {
                cylinder(h = 4, r = 8);
        }
}

PS: You might get the GUI more or less for free if you use xml configuration files, because I assume there are xml editors out there that can do what you would need and you just have to add a "execute solidpython script" button ;)

etjones commented 3 years ago

I think what I want really is a native OpenSCAD implementation of the Customizer syntax, so that generated OpenSCAD code can be changed dynamically in the OpenSCAD viewer, and so that projects created with SolidPython could be customized on Thingiverse.

I have a working prototype for OpenSCAD's sliders and dropdown boxes, and I should have checkbox & spinbox variants shortly. Because the OpenSCAD customizer syntax is so minimal, that part was simple. I had to make a custom subclass of float to work nicely with customizer variable math, and that took a little more doing, but I think it should be pretty low impact

jeff-dh commented 3 years ago

What do you think about this:

from solid import *

# ======================================
# = this could be done in some library =
# ======================================
class CustomizerInterface:
    def __init__(self):
        self.header = ""

    def register(self, name, value, options=''):
        self.header += f'{name} = {value}; //{options}\n'

    def get(self, name):
        return scad_inline(name)
# ======================================

customizer = CustomizerInterface()

#register all the custom variables you want to use
customizer.register("objects", "4", "[2, 4, 6]")
customizer.register("side", "4")
customizer.register("cube_pos", "[5, 5, 5]")
customizer.register("cube_size", "5")
customizer.register("text", '"customize me!"' ,' ["customize me!", "Thank you!"]')

#use scad_inline to use them
scene = scad_inline("""
                    for (i = [1:objects]){
                        translate([2*i*side,0,0]){
                            cube(side);
                        }
                    }
                    """)

#use the customizer.get function to use them as parameters
scene += translate(customizer.get("cube_pos")) (
            cube(customizer.get("cube_size")))

scene += translate([0, -20, 0]) (
            text(customizer.get("text")))

scad_render_to_file(scene, file_header = customizer.header)

:smiley:

Works great with expsolid and I think it will also work for the master. You need the scad_inline function from #178.

jeff-dh commented 3 years ago
py_factor = 2
cube_size = customizer.get(f"sin(cube_size) * {py_factor}")

scene += translate(customizer.get("cube_pos")) (
            cube(cube_size))
jeff-dh commented 3 years ago
cube_size = customizer.get(f"cube_size - cube_pos[0] * {py_factor}")