diff --git a/solid/__init__.py b/solid/__init__.py index d7c9cb1f..02f78bd1 100644 --- a/solid/__init__.py +++ b/solid/__init__.py @@ -2,6 +2,7 @@ # from solid import * # from solid.utils import * from .solidpython import scad_render, scad_render_to_file +from .greedy_scad_interface import * from .solidpython import scad_render_animated, scad_render_animated_file from .solidpython import OpenSCADObject, IncludedOpenSCADObject from .objects import * diff --git a/solid/examples/greedy_scad_interface.py b/solid/examples/greedy_scad_interface.py new file mode 100644 index 00000000..8041d220 --- /dev/null +++ b/solid/examples/greedy_scad_interface.py @@ -0,0 +1,61 @@ +# ====================================================== +# = add relative path to the solid package to sys.path = +# ====================================================== +import sys +from pathlib import Path +solidPath = Path(__file__).absolute().parent.parent.parent.as_posix() +sys.path.insert(0, solidPath) +#====================================================== +from solid import * + +set_global_fn(32) +set_global_viewport_distance(abs(sin(get_animation_time() * 360)) * 10 + 5) +set_global_viewport_translation([0, -1, 0]) +set_global_viewport_rotation([63, 0, get_animation_time() * 360]) +set_global_viewport_fov(25) + +def funny_cube(): + customized_color = CustomizerDropdownVariable(name = "cube_color", + default_value = "blue", + options = ["red", "green", "blue"], + label = "The color of the cube", + tab="Colors") + + customized_animation_factor = CustomizerSliderVariable(name = "anim_factor", + default_value = 1, + min_ = 1, + max_ = 10, + step = 0.5, + label = "Animation speed factor", + tab = "Animation") + + return color(customized_color) ( + cube(abs(sin(get_animation_time() * 360 * customized_animation_factor)), center=True) + ) + +def funny_sphere(): + customized_color = ScadValue("cube_color") + customized_animation_factor = ScadValue("anim_factor") + + return translate([0, -2, 0]) ( + color(customized_color) ( + sphere(r = abs(sin(get_animation_time() * 360 * customized_animation_factor - 90))) + ) + ) + +def do_nots(): + customized_color = ScadValue("cube_color") + customized_animation_factor = ScadValue("anim_factor") + + #if customized_color == "blue": + # print("This causes a python runtime error!") + + #for i in range(customized_animation_factor): + # print("This causes a python runtime error!") + + #f = 1.0 + #f *= customized_animation_factor + #for i in range(f): + # print("This causes a python runtime error! (and this is why it is called greedy)") + +scad_render_to_file(funny_cube() + funny_sphere()) diff --git a/solid/greedy_scad_interface/__init__.py b/solid/greedy_scad_interface/__init__.py new file mode 100644 index 00000000..f30a4f96 --- /dev/null +++ b/solid/greedy_scad_interface/__init__.py @@ -0,0 +1,4 @@ +from .customizer_widgets import * +from .scad_variable import * +from .scad_interface import * + diff --git a/solid/greedy_scad_interface/customizer_widgets.py b/solid/greedy_scad_interface/customizer_widgets.py new file mode 100644 index 00000000..9270c1ab --- /dev/null +++ b/solid/greedy_scad_interface/customizer_widgets.py @@ -0,0 +1,31 @@ +from .scad_variable import ScadVariable + +class CustomizerDropdownVariable(ScadVariable): + def __init__(self, name, default_value, options='', label='', tab=''): + if isinstance(options, list): + options_str = '[' + ", ".join(map(str, options)) + ']' + + if isinstance(options, dict): + reverse_options = [ f'{options[k]} : "{k}"' for k in options.keys()] + options_str = f'[{", ".join(reverse_options)}]' + + super().__init__(name, default_value, options_str, label=label, tab=tab) + +class CustomizerSliderVariable(ScadVariable): + def __init__(self, name, default_value, min_, max_, step='', label='', tab=''): + options_str = '[' + options_str += min_ and str(min_) + ':' + options_str += step and str(step) + ':' + options_str += str(max_) + ']' + + super().__init__(name, default_value, options_str, label=label, tab=tab) + +class CustomizerCheckboxVariable(ScadVariable): + def __init__(self, name, default_value, label='', tab=''): + super().__init__(name, default_value, label=label, tab=tab) + +class CustomizerTextboxVariable(ScadVariable): + def __init__(self, name, default_value, max_length='', label='', tab=''): + options_str = max_length and str(max_length) + super().__init__(name, default_value, options_str, label=label, tab=tab) + diff --git a/solid/greedy_scad_interface/scad_interface.py b/solid/greedy_scad_interface/scad_interface.py new file mode 100644 index 00000000..d92e5aa4 --- /dev/null +++ b/solid/greedy_scad_interface/scad_interface.py @@ -0,0 +1,30 @@ +from .scad_variable import ScadVariable, ScadValue + +def get_animation_time(): + return ScadValue("$t") + +def set_global_fn(_fn): + ScadVariable("$fn", _fn) + +def set_global_fa(_fa): + ScadVariable("$fa", _fa) + +def set_global_fs(_fs): + ScadVariable("$fs", _fs) + +def set_global_viewport_translation(trans): + ScadVariable("$vpt", trans) + +def set_global_viewport_rotation(rot): + ScadVariable("$vpr", rot) + +def set_global_viewport_fov(fov): + ScadVariable("$vpf", fov) + +def set_global_viewport_distance(d): + ScadVariable("$vpd", d) + +def get_scad_header(): + base_str = "\n\n".join(ScadVariable.registered_variables.values()) + return f'{base_str}\n' + diff --git a/solid/greedy_scad_interface/scad_variable.py b/solid/greedy_scad_interface/scad_variable.py new file mode 100644 index 00000000..4bf59e58 --- /dev/null +++ b/solid/greedy_scad_interface/scad_variable.py @@ -0,0 +1,87 @@ +import math + +sin = lambda x: ScadValue(f'sin({x})') if isinstance(x, ScadValue) else math.sin(x) +cos = lambda x: ScadValue(f'cos({x})') if isinstance(x, ScadValue) else math.cos(x) +tan = lambda x: ScadValue(f'tan({x})') if isinstance(x, ScadValue) else math.tan(x) +asin = lambda x: ScadValue(f'asin({x})') if isinstance(x, ScadValue) else math.asin(x) +acos = lambda x: ScadValue(f'acos({x})') if isinstance(x, ScadValue) else math.acos(x) +atan = lambda x: ScadValue(f'atan({x})') if isinstance(x, ScadValue) else math.atan(x) +sqrt = lambda x: ScadValue(f'sqrt({x})') if isinstance(x, ScadValue) else math.sqrt(x) +not_ = lambda x: ScadValue(f'!{x}') + +class ScadValue: + def __init__(self, value): + self.value = value + + def __repr__(self): + return f'{self.value}' + + def __operator_base__(self, op, other): + return ScadValue(f'({self} {op} {other})') + + def __unary_operator_base__(self, op): + return ScadValue(f'({op}{self})') + + def __illegal_operator__(self): + raise Exception("You can't compare a ScadValue with something else, " +\ + "because we don't know the customized value at " +\ + "SolidPythons runtime because it might get customized " +\ + "at OpenSCAD runtime.") + + #basic operators +, -, *, /, %, ** + def __add__(self, other): return self.__operator_base__("+", other) + def __sub__(self, other): return self.__operator_base__("-", other) + def __mul__(self, other): return self.__operator_base__("*", other) + def __mod__(self, other): return self.__operator_base__("%", other) + def __pow__(self, other): return self.__operator_base__("^", other) + def __truediv__(self, other): return self.__operator_base__("/", other) + + def __radd__(self, other): return self.__operator_base__("+", other) + def __rsub__(self, other): return self.__operator_base__("-", other) + def __rmul__(self, other): return self.__operator_base__("*", other) + def __rmod__(self, other): return self.__operator_base__("%", other) + def __rpow__(self, other): return self.__operator_base__("^", other) + def __rtruediv__(self, other): return self.__operator_base__("/", other) + + #unary operators + def __neg__(self): return self.__unary_operator_base__("-") + + #other operators + def __abs__(self): return ScadValue(f'abs({self})') + + #"illegal" operators + def __eq__(self, other): return self.__illegal_operator__() + def __ne__(self, other): return self.__illegal_operator__() + def __le__(self, other): return self.__illegal_operator__() + def __ge__(self, other): return self.__illegal_operator__() + def __lt__(self, other): return self.__illegal_operator__() + def __gt__(self, other): return self.__illegal_operator__() + + #do not allow to evaluate to bool + def __bool__(self): + raise Exception("You can't use scad variables as truth statement because " +\ + "we don't know the value of a customized variable at " +\ + "SolidPython runtime.") + +class ScadVariable(ScadValue): + registered_variables = {} + + def __init__(self, name, default_value, options_str='', label='', tab=''): + super().__init__(name) + + if name in self.registered_variables.keys(): + raise ValueError("Multiple instances of ScadVariable with the same name.") + + def_str = self.get_definition(name, default_value, options_str, label, tab) + self.registered_variables.update({name : def_str}) + + def get_definition(self, name, default_value, options_str, label, tab): + from ..solidpython import py2openscad + + tab = tab and f'/* [{tab}] */\n' + label = label and f'//{label}\n' + options_str = options_str and f' //{options_str}' + default_value = py2openscad(default_value) + + return f'{tab}{label}{name} = {default_value};{options_str}' + diff --git a/solid/solidpython.py b/solid/solidpython.py index 4fc935f2..421bd0c9 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -26,6 +26,8 @@ import pkg_resources import re +from . import greedy_scad_interface + PathStr = Union[Path, str] AnimFunc = Callable[[Optional[float]], 'OpenSCADObject'] # These are features added to SolidPython but NOT in OpenSCAD. @@ -427,7 +429,7 @@ def scad_render(scad_object: OpenSCADObject, file_header: str = '') -> str: if file_header and not file_header.endswith('\n'): file_header += '\n' - return file_header + includes + scad_body + return file_header + greedy_scad_interface.get_scad_header() + includes + scad_body def scad_render_animated(func_to_animate: AnimFunc, steps: int =20,