diff --git a/solid/customizer.py b/solid/customizer.py new file mode 100644 index 00000000..61bdfc79 --- /dev/null +++ b/solid/customizer.py @@ -0,0 +1,277 @@ +#! /usr/bin/env python3 +import sys + +from numbers import Real + +from typing import Callable, Union, Dict, List, Sequence, Any, Set +from typing import Generator, TypeVar + +Operator = Any + +class CompoundFloat(float): + ''' + CompoundFloat is a subclass of float that lets a float reconstruct the math + used to generate it. + + In normal python, `my_float = 3.0 + 1` yields 4.0, with no way to track how it was created. + A CompoundFloat contains, recursively, all float operations used to create it + compound = CompoundFloat(3.0) + new_compound = compound + 1 + # new_compound has the value 4.0, but str(new_compound) is `((3.0) + 1)` + + This allows us to use OpenSCAD's Customizer feature while still doing math + with the values, so that CustomizerSlider (a subclass of CompoundFloat) + does the following: + ```python + slider = CustomizerSlider('sliderName', 2, 1, 5, 1) + quad_box = cube([slider * slider, slider, slider]) + print(scad_render(quad_box)) + ``` + yields: + ``` + sliderName = 2; // [1:1:5] + cube(size = [(sliderName * sliderName), slider, slider]); + ``` + which can be adjusted interactively with OpenSCAD's or Thingiverse's Customizer feature + + ''' + def __new__(cls, val: Real, op: Operator = None, other: Real = None): + evaluated = val + if op and other: + evaluated = op(val, other) + self = super(CompoundFloat, cls).__new__(cls, evaluated) + self.val = val + # TODO: ensure that either op & other are defined, or neither are + self.op = op + self.other = other + return self + + def op_string(self) -> str: + # FIXME: add support for unary operators and non-infix ops like __ceil__() + symbols = { + '__add__': '+', + '__eq__': '==', + '__floordiv__': '//', + '__le__': '<=', + '__lt__': '<', + '__mod__': '%', + '__mul__': '*', + '__neg__': '-', + '__pos__': '+', + '__radd__': '+', + '__rfloordiv__': '//', + '__rmod__': '%', + '__rmul__': '*', + '__rpow__': '**', + '__rsub__': '-', + '__rtruediv__': '/', + '__truediv__': '/', + '__sub__': '-', + # '__ceil__': '', + # '__float__': '', + # '__floor__': '', + # '__pow__': '**', # NOTE: OpenSCAD uses `pow(a,b)`, not `a ** b` + # '__round__': '', + # '__trunc__': '', + + } + if self.op: + result = symbols.get(self.op.__name__, '') + else: + result = '' + return result + + def __str__(self) -> str: + val_str = f'{self.val}' + op_str = f' {self.op_string()}' if self.op else '' + other_str = f' {self.other}' if self.other else '' + return f'({val_str}{op_str}{other_str})' + + def customizer_instances(self) -> List['Customizer']: + # Recursively visit the parts of this compound, returning a list of all + # instances of Customizer subclasses + customizers = [] + elements = (self.val, self.other) + for elt in elements: + if isinstance(elt, Customizer): + customizers.append(elt) + elif isinstance(elt, CompoundFloat): + customizers += elt.customizer_instances() + return customizers + + def new_compound_float_for_operator(self, other:Real): + # NOTE: by reaching back into the stack for the calling function every time, + # this is slower than it would be with a custom function for each operator. + # But it's rare that SolidPython programs take any time to run, and this + # enables a more general code solution + op_name = sys._getframe(1).f_code.co_name + operator = getattr(float, op_name) + return CompoundFloat(self, operator, other) + + def __add__(self, other:Real): return self.new_compound_float_for_operator(other) + def __ceil__(self, other:Real): return self.new_compound_float_for_operator(other) + def __eq__(self, other:Real): return self.new_compound_float_for_operator(other) + def __float__(self, other:Real): return self.new_compound_float_for_operator(other) + def __floor__(self, other:Real): return self.new_compound_float_for_operator(other) + def __floordiv__(self, other:Real): return self.new_compound_float_for_operator(other) + def __le__(self, other:Real): return self.new_compound_float_for_operator(other) + def __lt__(self, other:Real): return self.new_compound_float_for_operator(other) + def __mod__(self, other:Real): return self.new_compound_float_for_operator(other) + def __mul__(self, other:Real): return self.new_compound_float_for_operator(other) + def __pow__(self, other:Real): return self.new_compound_float_for_operator(other) + def __radd__(self, other:Real): return self.new_compound_float_for_operator(other) + def __rmod__(self, other:Real): return self.new_compound_float_for_operator(other) + def __rmul__(self, other:Real): return self.new_compound_float_for_operator(other) + def __round__(self, other:Real): return self.new_compound_float_for_operator(other) + def __sub__(self, other:Real): return self.new_compound_float_for_operator(other) + def __truediv__(self, other:Real): return self.new_compound_float_for_operator(other) + def __trunc__(self, other:Real): return self.new_compound_float_for_operator(other) + # right- operators + def __rpow__(self, other:Real): return CompoundFloat(float(other), float.__pow__, self) + def __rfloordiv__(self, other:Real):return CompoundFloat(float(other), float.__rfloordiv__, self) + def __rsub__(self, other:Real): return CompoundFloat(float(other), float.__sub__, self) + def __rtruediv__(self, other:Real): return CompoundFloat(float(other), float.__truediv__, self) + # unary operators + def __neg__(self): return CompoundFloat(-1.0, float.__mul__, self) + def __pos__(self): return self + +class CompundInt(int): + pass + +class CompoundBoolean(int): + pass + +class CompoundString(str): + pass + +class Customizer(): + ''' + Base class for all Customizer UI elements. + ''' + def scad_declaration(self): + raise NotImplementedError + + def __str__(self): + return self.name + +class CustomizerSlider(CompoundFloat, Customizer): + def __new__(cls, name:str, val:float, min_val:float=0, max_val:float = 1, step:float = None): + inst = super(CustomizerSlider, cls).__new__(cls, val) + inst.val = val + inst.name = name + inst.min = min_val + inst.max = max_val + inst.step = step + return inst + + def scad_declaration(self) -> str: + # OpenSCAD's customizer makes a guess about step size if not supplied. + # Leave it out in that case and let OpenSCAD's logic handle it + step_str = f': {self.step}' if self.step else '' + return f'{self.name} = {self.val}; // [{self.min}{step_str}: {self.max}]\n' + + def __str__(self) -> str: + return self.name + +class CustomizerDropdownNumber(CompoundFloat, Customizer): + def __new__(cls, name:str, val:float, options:Union[List[float], Dict[float,str]]): + self = super(CustomizerDropdownNumber, cls).__new__(cls, val) + self.name = name + self.val = val + # TODO: validate that all entries in options are floats, or + # that self.options is a Dict[float, str] + self.options = options + return self + + def scad_declaration(self): + options_str = f'{self.options}' + if isinstance(self.options, dict): + dict_pairs = [f'{k}:{v}' for k,v in self.options.items()] + options_str = '[' + ', '.join(dict_pairs) + ']' + return f'{self.name} = {self.val}; // {options_str}\n' + + def __str__(self): + return self.name + +class CustomizerSpinbox(CompoundFloat, Customizer): + # NOTE: if val is an integer (1, or 1.0), each spin step will be 1 + # If val is a decimal, spin step will be one decimal unit, + # i.e. 5.5 -> 0.1 step, 5.002 => 0.001 step + def __new__(cls, name:str, val:float): + self = super(CustomizerSpinbox, cls).__new__(cls, val) + self.name = name + self.val = val + return self + + def scad_declaration(self): + # No special comment syntax required for spinboxes + return f'{self.name} = {self.val};\n' + + def __str__(self): + return self.name + +class CustomizerDropdownString(CompoundString, Customizer): + def __new__(cls, name:str, val:str, options:Union[List[str], Dict[str,str]]): + self = super(CustomizerDropdownString, cls).__new__(cls, val) + self.name = name + self.val = val + # TODO: validate that all entries in options are floats, or + # that self.options is a Dict[str, float] + self.options = options + return self + + def scad_declaration(self): + if isinstance(self.options, dict): + opts = [f'{k}:{v}' for k,v in self.options.items()] + else: + opts = self.options + options_str = '[' + ', '.join(opts) + ']' + return f'{self.name} = "{self.val}"; // {options_str}\n' + + def __str__(self): + return self.name + +class CustomizerTextbox(CompoundString, Customizer): + def __new__(cls, name:str, val:str): + self = super(CustomizerTextbox, cls).__new__(cls, val) + self.name = name + self.val = val + return self + + def scad_declaration(self): + # No special comment syntax required for textboxes + return f'{self.name} = "{self.val}";\n' + + def __str__(self): + return self.name + +class CustomizerCheckbox(CompoundBoolean, Customizer): + def __new__(cls, name:str, val:bool): + self = super(CustomizerCheckbox, cls).__new__(cls, val) + self.name = name + self.val = val + return self + + def scad_declaration(self): + # No special comment syntax required for checkboxes + val_str = f'{self.val}'.lower() + return f'{self.name} = {val_str};\n' + + def __str__(self): + return self.name + +# TODO: Support OpenSCAD Parameter tabs, with the syntax: +# '/* [$TAB_NAME] */' +# Note that this would require either asking Python `CustomizerTab` +# objects to explicitly add Customizer elements or some +# other mechanism for ordering parameters, since all OpenSCAD +# customizer params are at the top module level and hierarchy +# is described by order of elements + + +# TODO: write CustomizerVector, which is a list of number +# spinboxes (with optional ranges) for 0-4 numbers, or a +# text field for 5+ numbers +class CustomizerVector(Customizer): + def __str__(self): + return self.name diff --git a/solid/examples/basic_customizer.py b/solid/examples/basic_customizer.py new file mode 100644 index 00000000..9f1467b4 --- /dev/null +++ b/solid/examples/basic_customizer.py @@ -0,0 +1,96 @@ +#! /usr/bin/env python3 + +from solid import (cylinder, rotate, translate, scad_render_to_file, union, cube, text) +from solid.customizer import (CustomizerCheckbox, CustomizerDropdownString, CustomizerSlider, + CustomizerDropdownNumber, CustomizerSpinbox, CustomizerTextbox) +SEGMENTS = 48 + +def ported_scad_example(): + elts = [] + + # // combo box for number + # SCAD: a_numbers_dropdown = 4; // [2, 4, 12, 14] + a_numbers_dropdown = CustomizerDropdownNumber('a_numbers_dropdown', 4, [2, 4, 12, 14]) + elts.append (cube(a_numbers_dropdown)) + + # // combo box for string + # SCAD: b_strings_dropdown = "foo"; // [foo, bar, baz] + b_strings_dropdown = CustomizerDropdownString('b_strings_dropdown', 'foo', ['foo', 'bar', 'baz'] ) + elts.append( text(b_strings_dropdown)) + + # // labeled combo box for string + # Strings = "M"; // [] + + # // slider widget for number + # SCAD: slider =34; // [10:100] + c_slider = CustomizerSlider('c_slider', 3, 1, 10) + elts.append( cube(c_slider)) + + # //step slider for number + # SCAD: stepSlider=2; //[0:5:100] + d_step_slider = CustomizerSlider('d_step_slider', 2, 0, 100, 5) + elts.append( cube(d_step_slider)) + + # Checkbox + # SCAD: e_checkbox = true; + e_checkbox = CustomizerCheckbox('e_checkbox', True) + elts.append( cube(size=5, center=e_checkbox)) + + # Spinbox with step size 1 + # SCAD: f_spinbox = 5; + f_spinbox = CustomizerSpinbox('f_spinbox', 5) + elts.append( cube(f_spinbox)) + + # // spinbox with step size 0.01 + # SCAD: g_float_spinbox = 5.1; + g_float_spinbox = CustomizerSpinbox('g_float_spinbox', 5.1) + elts.append( cube(g_float_spinbox)) + + # Textbox: + # // Text box for string + # SCAD: h_textbox = "hello"; + h_textbox = CustomizerTextbox('h_textbox', 'hello') + elts.append( text(h_textbox)) + + # TODO: compound Spinboxes for 1-4 numbers, & textbox for 5+ numbers + + # Move everything down in a line from y=0 so we can see the different objects + elts = [translate([0,-10*i,0])(e) for i,e in enumerate(elts)] + + # And return everything; Customizer instances that aren't used in OpenSCAD + # objects and rendered won't appear in the final SCAD file + a = union()( elts ) + return a + +def custom_cube(): + side_length = CustomizerSlider('sideLength', 1, min_val=1, max_val=10, step=1) + offset= CustomizerSlider('offset', 2, 0, 10, 2) + angle = CustomizerSlider('angle', 0, 0, 90) # Note no step value; OpenSCAD supplies + + # Once you've defined your Customizer objects, use them naturally like you + # would Python variables. SolidPython will put the correct OpenSCAD code at + # the beginning of the file so that the GUI is defined. + a = rotate([0, 0, angle])( + translate([offset, offset, 0])( + cylinder(r1=2*side_length, r2=side_length, h=3*side_length) + ) + ) + + return a + +# FIXME: include examples of ways we *can't* use Customizer objects just like +# Python variables. Basically, you can use a Customizer as an argument to an OpenSCAD +# function, but if you're doing pure Python things, OpenSCAD won't pick up on the +# use of a Customizer variable. +# E.g. ``` +# slider_val = CustomizedSlider('slider_val', val=4, min_val=2, max_val=10) +# objs = [some_obj(i) for i in range(slider_val)] +# ``` +# This will always return 4 objects, since the list comprehension is pure Python +# and won't be passed on to OpenSCAD. + +if __name__ == '__main__': + a = custom_cube() + a = ported_scad_example() + out_path = scad_render_to_file(a, file_header='$fn = %s;' % SEGMENTS, include_orig_code=True) + print(f'Wrote file to {out_path}') \ No newline at end of file diff --git a/solid/objects.py b/solid/objects.py index 79aff238..794e9853 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -832,7 +832,7 @@ def _openscad_library_paths() -> List[Path]: default_paths = { 'Linux': Path.home() / '.local/share/OpenSCAD/libraries', 'Darwin': Path.home() / 'Documents/OpenSCAD/libraries', - 'Windows': Path('My Documents\OpenSCAD\libraries') + 'Windows': Path('My Documents/OpenSCAD/libraries') } paths.append(default_paths[platform.system()]) diff --git a/solid/py_scadparser/parsetab.py b/solid/py_scadparser/parsetab.py index 9e2b20a9..12e93e78 100644 --- a/solid/py_scadparser/parsetab.py +++ b/solid/py_scadparser/parsetab.py @@ -6,7 +6,7 @@ _lr_method = 'LALR' -_lr_signature = 'nonassocASSERTnonassocECHOnonassocTHENnonassocELSEnonassoc?nonassoc:nonassoc(){}nonassoc=leftANDORleftEQUALNOT_EQUALGREATER_OR_EQUALLESS_OR_EQUAL>" expression\n | expression EQUAL expression\n | expression NOT_EQUAL expression\n | expression GREATER_OR_EQUAL expression\n | expression LESS_OR_EQUAL expression\n | expression AND expression\n | expression OR expression\n access_expr : ID %prec ACCESS\n | expression "." ID %prec ACCESS\n | expression "(" call_parameter_list ")" %prec ACCESS\n | expression "(" ")" %prec ACCESS\n | expression "[" expression "]" %prec ACCESS\n list_stuff : FUNCTION "(" opt_parameter_list ")" expression\n | LET "(" assignment_list ")" expression %prec THEN\n | EACH expression %prec THEN\n | "[" expression ":" expression "]"\n | "[" expression ":" expression ":" expression "]"\n | "[" for_loop expression "]"\n | tuple\n assert_or_echo : ASSERT "(" opt_call_parameter_list ")"\n | ECHO "(" opt_call_parameter_list ")"\n constants : STRING\n | TRUE\n | FALSE\n | NUMBERopt_else : \n | ELSE expression %prec THEN\n for_or_if : for_loop expression %prec THEN\n | IF "(" expression ")" expression opt_else\n expression : access_expr\n | logic_expr\n | list_stuff\n | assert_or_echo\n | assert_or_echo expression %prec ASSERT\n | constants\n | for_or_if\n | "(" expression ")"\n assignment_list : ID "=" expression\n | assignment_list "," ID "=" expression\n call : ID "(" call_parameter_list ")"\n | ID "(" ")" tuple : "[" opt_expression_list "]"\n commas : commas ","\n | ","\n opt_expression_list : expression_list\n | expression_list commas\n | empty expression_list : expression_list commas expression\n | expression\n opt_call_parameter_list :\n | call_parameter_list\n call_parameter_list : call_parameter_list commas call_parameter\n | call_parametercall_parameter : expression\n | ID "=" expressionopt_parameter_list : parameter_list\n | parameter_list commas\n | empty\n parameter_list : parameter_list commas parameter\n | parameterparameter : ID\n | ID "=" expressionfunction : FUNCTION ID "(" opt_parameter_list ")" "=" expression\n module : MODULE ID "(" opt_parameter_list ")" statement\n ' +_lr_signature = 'nonassocASSERTnonassocECHOnonassocTHENnonassocELSEnonassoc?nonassoc:nonassoc(){}nonassoc=leftANDORleftEQUALNOT_EQUALGREATER_OR_EQUALLESS_OR_EQUAL>" expression\n | expression EQUAL expression\n | expression NOT_EQUAL expression\n | expression GREATER_OR_EQUAL expression\n | expression LESS_OR_EQUAL expression\n | expression AND expression\n | expression OR expression\n access_expr : ID %prec ACCESS\n | expression "." ID %prec ACCESS\n | expression "(" call_parameter_list ")" %prec ACCESS\n | expression "(" ")" %prec ACCESS\n | expression "[" expression "]" %prec ACCESS\n list_stuff : FUNCTION "(" opt_parameter_list ")" expression\n | LET "(" assignment_list ")" expression %prec THEN\n | EACH expression %prec THEN\n | "[" expression ":" expression "]"\n | "[" expression ":" expression ":" expression "]"\n | "[" for_loop expression "]"\n | tuple\n assert_or_echo : ASSERT "(" opt_call_parameter_list ")"\n | ECHO "(" opt_call_parameter_list ")"\n constants : STRING\n | TRUE\n | FALSE\n | NUMBERopt_else :\n | ELSE expression %prec THEN\n for_or_if : for_loop expression %prec THEN\n | IF "(" expression ")" expression opt_else\n expression : access_expr\n | logic_expr\n | list_stuff\n | assert_or_echo\n | assert_or_echo expression %prec ASSERT\n | constants\n | for_or_if\n | "(" expression ")"\n assignment_list : ID "=" expression\n | assignment_list "," ID "=" expression\n call : ID "(" call_parameter_list ")"\n | ID "(" ")" tuple : "[" opt_expression_list "]"\n commas : commas ","\n | ","\n opt_expression_list : expression_list\n | expression_list commas\n | empty expression_list : expression_list commas expression\n | expression\n opt_call_parameter_list :\n | call_parameter_list\n call_parameter_list : call_parameter_list commas call_parameter\n | call_parametercall_parameter : expression\n | ID "=" expressionopt_parameter_list : parameter_list\n | parameter_list commas\n | empty\n parameter_list : parameter_list commas parameter\n | parameterparameter : ID\n | ID "=" expressionfunction : FUNCTION ID "(" opt_parameter_list ")" "=" expression\n module : MODULE ID "(" opt_parameter_list ")" statement\n ' _lr_action_items = {'IF':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,24,25,27,28,29,30,31,32,33,34,35,36,37,38,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,181,182,183,184,185,186,188,190,191,192,193,194,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,4,-2,-1,4,-3,4,4,4,4,4,-18,-21,-22,42,-6,42,42,4,-11,-12,-13,-14,-15,-16,-17,42,42,42,-64,-65,-66,42,-69,-70,-42,42,42,42,42,42,42,-53,-56,-57,-58,-59,-10,-75,42,42,4,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,-68,42,-24,-25,-26,-49,-62,42,42,4,42,4,42,-78,42,4,-23,-74,-19,42,42,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,42,-76,42,-7,-8,-77,-9,4,42,-44,4,-46,42,-52,42,42,-54,-55,42,42,-98,-60,-5,-27,42,-50,-47,-48,-97,-63,42,-20,-61,-51,]),'LET':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,24,25,27,28,29,30,31,32,33,34,35,36,37,38,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,181,182,183,184,185,186,188,190,191,192,193,194,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,6,-2,-1,6,-3,6,6,6,6,6,-18,-21,-22,57,-6,57,57,6,-11,-12,-13,-14,-15,-16,-17,57,57,57,-64,-65,-66,57,-69,-70,-42,57,57,57,57,57,57,-53,-56,-57,-58,-59,-10,-75,57,57,6,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,-68,57,-24,-25,-26,-49,-62,57,57,6,57,6,57,-78,57,6,-23,-74,-19,57,57,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,57,-76,57,-7,-8,-77,-9,6,57,-44,6,-46,57,-52,57,57,-54,-55,57,57,-98,-60,-5,-27,57,-50,-47,-48,-97,-63,57,-20,-61,-51,]),'ASSERT':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,24,25,27,28,29,30,31,32,33,34,35,36,37,38,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,181,182,183,184,185,186,188,190,191,192,193,194,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,7,-2,-1,7,-3,7,7,7,7,7,-18,-21,-22,61,-6,61,61,7,-11,-12,-13,-14,-15,-16,-17,61,61,61,-64,-65,-66,61,-69,-70,-42,61,61,61,61,61,61,-53,-56,-57,-58,-59,-10,-75,61,61,7,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,-68,61,-24,-25,-26,-49,-62,61,61,7,61,7,61,-78,61,7,-23,-74,-19,61,61,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,61,-76,61,-7,-8,-77,-9,7,61,-44,7,-46,61,-52,61,61,-54,-55,61,61,-98,-60,-5,-27,61,-50,-47,-48,-97,-63,61,-20,-61,-51,]),'ECHO':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,24,25,27,28,29,30,31,32,33,34,35,36,37,38,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,181,182,183,184,185,186,188,190,191,192,193,194,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,8,-2,-1,8,-3,8,8,8,8,8,-18,-21,-22,62,-6,62,62,8,-11,-12,-13,-14,-15,-16,-17,62,62,62,-64,-65,-66,62,-69,-70,-42,62,62,62,62,62,62,-53,-56,-57,-58,-59,-10,-75,62,62,8,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,-68,62,-24,-25,-26,-49,-62,62,62,8,62,8,62,-78,62,8,-23,-74,-19,62,62,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,62,-76,62,-7,-8,-77,-9,8,62,-44,8,-46,62,-52,62,62,-54,-55,62,62,-98,-60,-5,-27,62,-50,-47,-48,-97,-63,62,-20,-61,-51,]),'{':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,78,87,105,111,112,113,116,117,120,123,127,128,129,130,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,181,183,184,185,188,192,193,197,198,199,200,202,203,204,207,208,211,212,213,],[-3,9,-2,-1,9,-3,9,9,9,9,9,-18,-21,-22,-6,9,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-75,9,-68,-24,-25,-26,-49,-62,9,9,9,-23,-74,-19,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,9,-44,9,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-20,-61,-51,]),'%':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,75,76,78,85,87,105,106,111,112,113,116,117,120,123,127,128,129,130,138,139,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,168,170,171,174,175,176,178,181,183,184,185,187,188,189,192,193,197,198,199,200,202,203,204,205,207,208,210,211,212,213,],[-3,10,-2,-1,10,-3,10,10,10,10,10,-18,-21,-22,-6,10,-11,-12,-13,-14,-15,-16,-17,91,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,91,-42,-10,91,-75,91,10,91,91,-24,-25,-26,91,91,10,10,10,-23,-74,-19,91,-71,-45,-4,-43,91,91,-28,91,91,-31,-32,-33,91,91,91,91,91,91,91,91,91,-76,-7,91,-8,91,-9,91,91,10,-44,10,-46,91,-52,91,-54,-55,-98,91,-5,91,-50,91,91,91,91,-63,91,-20,91,-51,]),'*':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,75,76,78,85,87,105,106,111,112,113,116,117,120,123,127,128,129,130,138,139,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,168,170,171,174,175,176,178,181,183,184,185,187,188,189,192,193,197,198,199,200,202,203,204,205,207,208,210,211,212,213,],[-3,11,-2,-1,11,-3,11,11,11,11,11,-18,-21,-22,-6,11,-11,-12,-13,-14,-15,-16,-17,95,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,95,-42,-10,95,-75,95,11,95,95,-24,-25,-26,95,95,11,11,11,-23,-74,-19,95,-71,-45,-4,-43,95,95,95,95,95,-31,-32,-33,95,95,95,95,95,95,95,95,95,-76,-7,95,-8,95,-9,95,95,11,-44,11,-46,95,-52,95,-54,-55,-98,95,-5,95,-50,95,95,95,95,-63,95,-20,95,-51,]),'!':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,24,25,27,28,29,30,31,32,33,34,35,36,37,38,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,181,182,183,184,185,186,188,190,191,192,193,194,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,12,-2,-1,12,-3,12,12,12,12,12,-18,-21,-22,55,-6,55,55,12,-11,-12,-13,-14,-15,-16,-17,55,55,55,-64,-65,-66,55,-69,-70,-42,55,55,55,55,55,55,-53,-56,-57,-58,-59,-10,-75,55,55,12,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,-68,55,-24,-25,-26,-49,-62,55,55,12,55,12,55,-78,55,12,-23,-74,-19,55,55,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,55,-76,55,-7,-8,-77,-9,12,55,-44,12,-46,55,-52,55,55,-54,-55,55,55,-98,-60,-5,-27,55,-50,-47,-48,-97,-63,55,-20,-61,-51,]),'#':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,78,87,105,111,112,113,116,117,120,123,127,128,129,130,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,181,183,184,185,188,192,193,197,198,199,200,202,203,204,207,208,211,212,213,],[-3,13,-2,-1,13,-3,13,13,13,13,13,-18,-21,-22,-6,13,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-75,13,-68,-24,-25,-26,-49,-62,13,13,13,-23,-74,-19,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,13,-44,13,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-20,-61,-51,]),'USE':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,78,87,105,111,112,113,116,117,120,123,127,128,129,130,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,181,183,184,185,188,192,193,197,198,199,200,202,203,204,207,208,211,212,213,],[-3,15,-2,-1,15,-3,15,15,15,15,15,-18,-21,-22,-6,15,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-75,15,-68,-24,-25,-26,-49,-62,15,15,15,-23,-74,-19,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,15,-44,15,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-20,-61,-51,]),'INCLUDE':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,78,87,105,111,112,113,116,117,120,123,127,128,129,130,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,181,183,184,185,188,192,193,197,198,199,200,202,203,204,207,208,211,212,213,],[-3,16,-2,-1,16,-3,16,16,16,16,16,-18,-21,-22,-6,16,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-75,16,-68,-24,-25,-26,-49,-62,16,16,16,-23,-74,-19,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,16,-44,16,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-20,-61,-51,]),';':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,76,78,79,80,81,87,105,111,112,113,116,117,120,123,127,128,129,130,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,176,177,178,181,183,184,185,188,192,193,197,198,199,200,202,203,204,207,208,211,212,213,],[-3,17,-2,-1,17,-3,17,17,17,17,17,-18,-21,-22,-6,17,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,128,-75,131,-94,-95,17,-68,-24,-25,-26,-49,-62,17,17,17,-23,-74,-19,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,195,-93,-96,17,-44,17,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-20,-61,-51,]),'ID':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,82,83,84,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,180,181,182,183,184,185,186,188,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,20,-2,-1,20,-3,20,20,20,20,20,-18,-21,-22,40,41,51,-6,68,73,73,20,-11,-12,-13,-14,-15,-16,-17,51,73,81,51,-64,-65,-66,51,-69,-70,-42,51,51,51,51,51,51,-53,-56,-57,-58,-59,-10,-75,81,81,51,73,20,143,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,-68,51,-24,-25,-26,81,68,-49,-62,73,73,20,169,51,20,73,-78,51,20,-23,-74,-19,51,81,51,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,51,-76,51,-7,-8,-77,-9,81,20,51,-44,20,-46,51,-52,51,51,-54,-55,51,81,51,-98,-60,-5,-27,51,-50,-47,-48,-97,-63,51,-20,-61,-51,]),'FOR':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,24,25,27,28,29,30,31,32,33,34,35,36,37,38,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,181,182,183,184,185,186,188,190,191,192,193,194,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,21,-2,-1,21,-3,21,21,21,21,21,-18,-21,-22,21,-6,21,21,21,-11,-12,-13,-14,-15,-16,-17,21,21,21,-64,-65,-66,21,-69,-70,-42,21,21,21,21,21,21,-53,-56,-57,-58,-59,-10,-75,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,-68,21,-24,-25,-26,-49,-62,21,21,21,21,21,21,-78,21,21,-23,-74,-19,21,21,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,21,-76,21,-7,-8,-77,-9,21,21,-44,21,-46,21,-52,21,21,-54,-55,21,21,-98,-60,-5,-27,21,-50,-47,-48,-97,-63,21,-20,-61,-51,]),'FUNCTION':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,24,25,27,28,29,30,31,32,33,34,35,36,37,38,43,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,75,78,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,107,111,112,113,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,133,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,162,163,168,171,173,175,181,182,183,184,185,186,188,190,191,192,193,194,196,197,198,199,200,201,202,203,204,207,208,209,211,212,213,],[-3,22,-2,-1,22,-3,22,22,22,22,22,-18,-21,-22,56,-6,56,56,22,-11,-12,-13,-14,-15,-16,-17,56,56,56,-64,-65,-66,56,-69,-70,-42,56,56,56,56,56,56,-53,-56,-57,-58,-59,-10,-75,56,56,22,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,-68,56,-24,-25,-26,-49,-62,56,56,22,56,22,56,-78,56,22,-23,-74,-19,56,56,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,56,-76,56,-7,-8,-77,-9,22,56,-44,22,-46,56,-52,56,56,-54,-55,56,56,-98,-60,-5,-27,56,-50,-47,-48,-97,-63,56,-20,-61,-51,]),'MODULE':([0,1,2,3,5,9,10,11,12,13,14,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,78,87,105,111,112,113,116,117,120,123,127,128,129,130,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,181,183,184,185,188,192,193,197,198,199,200,202,203,204,207,208,211,212,213,],[-3,23,-2,-1,23,-3,23,23,23,23,23,-18,-21,-22,-6,23,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-75,23,-68,-24,-25,-26,-49,-62,23,23,23,-23,-74,-19,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,23,-44,23,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-20,-61,-51,]),'$end':([0,1,2,3,17,18,19,25,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,105,111,112,113,116,117,128,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,183,185,188,192,193,197,198,199,200,202,203,204,207,208,212,213,],[-3,0,-2,-1,-18,-21,-22,-6,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-68,-24,-25,-26,-49,-62,-23,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,-44,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-61,-51,]),'}':([2,3,9,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,105,111,112,113,116,117,128,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,183,185,188,192,193,197,198,199,200,202,203,204,207,208,212,213,],[-2,-1,-3,-18,-21,-22,-6,75,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-68,-24,-25,-26,-49,-62,-23,-71,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,-44,-46,-52,-54,-55,-98,-60,-5,-27,-50,-47,-48,-97,-63,-61,-51,]),'(':([4,6,7,8,20,21,24,27,28,37,38,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,72,73,76,84,85,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,111,112,113,116,117,118,119,122,124,125,126,130,131,133,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,170,173,174,176,178,182,183,185,186,187,188,189,190,191,192,193,194,196,198,200,201,202,203,204,205,207,208,209,210,211,212,213,],[24,26,27,28,38,39,43,43,43,43,43,82,83,84,43,86,-64,-65,-66,43,-69,-70,-42,43,43,43,43,114,115,43,43,-53,118,119,-56,-57,-58,-59,86,-42,86,43,86,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,86,86,43,-24,-25,-26,86,86,43,43,43,43,-78,43,-19,43,43,86,-71,-45,-43,86,86,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,43,86,-76,43,86,-77,86,86,86,43,-44,-46,43,86,-52,86,43,43,-54,-55,43,43,86,86,43,-50,86,86,86,86,-63,43,86,-20,86,-51,]),'FILENAME':([15,16,],[35,36,]),'ELSE':([17,18,19,25,30,31,32,33,34,35,36,45,46,47,48,49,50,51,60,63,64,65,66,75,105,111,112,113,116,117,128,139,141,142,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,168,171,175,183,185,188,192,193,197,198,199,200,202,203,204,207,208,212,213,],[-18,-21,-22,-6,-11,-12,-13,-14,-15,-16,-17,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-10,-68,-24,-25,-26,-49,-62,-23,-71,-45,184,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,-7,-8,-9,-44,-46,-52,-54,-55,-98,209,-5,-27,-50,-47,-48,-97,-63,-61,-51,]),'=':([20,68,73,81,169,179,],[37,122,126,133,194,196,]),'-':([24,27,28,37,38,43,44,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,72,73,76,84,85,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,111,112,113,116,117,118,119,122,124,125,126,130,131,133,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,170,173,174,176,178,182,183,185,186,187,188,189,190,191,192,193,194,196,198,200,201,202,203,204,205,207,208,209,210,211,212,213,],[53,53,53,53,53,53,93,-64,-65,-66,53,-69,-70,-42,53,53,53,53,53,53,-53,-56,-57,-58,-59,93,-42,93,53,93,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,93,93,53,-24,-25,-26,93,93,53,53,53,53,-78,53,-19,53,53,93,-71,-45,-43,93,93,-28,-29,-30,-31,-32,-33,93,93,93,93,93,93,93,93,53,93,-76,53,93,-77,93,93,93,53,-44,-46,53,93,-52,93,53,53,-54,-55,53,53,93,93,53,-50,93,93,93,93,-63,53,93,-20,93,-51,]),'+':([24,27,28,37,38,43,44,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,72,73,76,84,85,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,111,112,113,116,117,118,119,122,124,125,126,130,131,133,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,170,173,174,176,178,182,183,185,186,187,188,189,190,191,192,193,194,196,198,200,201,202,203,204,205,207,208,209,210,211,212,213,],[54,54,54,54,54,54,92,-64,-65,-66,54,-69,-70,-42,54,54,54,54,54,54,-53,-56,-57,-58,-59,92,-42,92,54,92,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,92,92,54,-24,-25,-26,92,92,54,54,54,54,-78,54,-19,54,54,92,-71,-45,-43,92,92,-28,-29,-30,-31,-32,-33,92,92,92,92,92,92,92,92,54,92,-76,54,92,-77,92,92,92,54,-44,-46,54,92,-52,92,54,54,-54,-55,54,54,92,92,54,-50,92,92,92,92,-63,54,92,-20,92,-51,]),'EACH':([24,27,28,37,38,43,48,52,53,54,55,58,59,84,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,122,124,125,126,130,131,133,160,163,173,182,186,190,191,192,193,194,196,201,209,211,],[58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,-78,58,-19,58,58,58,58,-77,58,58,58,58,-54,-55,58,58,58,58,-20,]),'[':([24,27,28,37,38,43,44,45,46,47,48,49,50,51,52,53,54,55,58,59,60,63,64,65,66,72,73,76,84,85,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,111,112,113,116,117,118,119,122,124,125,126,130,131,133,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,170,173,174,176,178,182,183,185,186,187,188,189,190,191,192,193,194,196,198,200,201,202,203,204,205,207,208,209,210,211,212,213,],[52,52,52,52,52,52,89,-64,-65,-66,52,-69,-70,-42,52,52,52,52,52,52,-53,-56,-57,-58,-59,89,-42,89,52,89,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,-68,89,52,-24,-25,-26,-49,-62,52,52,52,52,-78,52,-19,52,52,89,-71,-45,-43,89,89,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,52,-62,-76,52,89,-77,89,89,89,52,-44,-46,52,89,-52,89,52,52,-54,-55,52,52,89,-27,52,-50,-47,-48,89,89,-63,52,89,-20,-61,-51,]),'STRING':([24,27,28,37,38,43,48,52,53,54,55,58,59,84,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,122,124,125,126,130,131,133,160,163,173,182,186,190,191,192,193,194,196,201,209,211,],[63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,-78,63,-19,63,63,63,63,-77,63,63,63,63,-54,-55,63,63,63,63,-20,]),'TRUE':([24,27,28,37,38,43,48,52,53,54,55,58,59,84,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,122,124,125,126,130,131,133,160,163,173,182,186,190,191,192,193,194,196,201,209,211,],[64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,-78,64,-19,64,64,64,64,-77,64,64,64,64,-54,-55,64,64,64,64,-20,]),'FALSE':([24,27,28,37,38,43,48,52,53,54,55,58,59,84,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,122,124,125,126,130,131,133,160,163,173,182,186,190,191,192,193,194,196,201,209,211,],[65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,-78,65,-19,65,65,65,65,-77,65,65,65,65,-54,-55,65,65,65,65,-20,]),'NUMBER':([24,27,28,37,38,43,48,52,53,54,55,58,59,84,86,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,122,124,125,126,130,131,133,160,163,173,182,186,190,191,192,193,194,196,201,209,211,],[66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,-78,66,-19,66,66,66,66,-77,66,66,66,66,-54,-55,66,66,66,66,-20,]),')':([27,28,38,44,45,46,47,48,49,50,51,60,63,64,65,66,67,69,70,71,72,73,74,77,79,80,81,82,83,85,86,105,111,112,113,114,116,117,118,119,125,134,135,136,137,138,139,140,141,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,162,164,165,166,167,170,172,173,174,177,178,180,183,185,188,192,193,198,200,202,203,204,205,206,208,212,213,],[-84,-84,78,87,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,120,123,-85,-87,-88,-42,127,129,130,-94,-95,-3,-3,139,141,-68,-24,-25,-26,-3,-49,-62,-84,-84,-78,179,-90,-92,181,182,-71,183,-45,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-76,190,191,192,193,-72,-86,-77,-89,-93,-96,-91,-44,-46,-52,-54,-55,-60,-27,-50,-47,-48,-73,211,-63,-61,-51,]),'.':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[88,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,88,-42,88,88,-68,88,-24,-25,-26,-49,-62,88,-71,-45,-43,88,88,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-62,-76,88,88,88,88,-44,-46,88,-52,88,-54,-55,88,-27,-50,-47,-48,88,88,-63,88,-61,-51,]),'?':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[90,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,90,-42,90,90,90,90,-24,-25,-26,90,90,90,-71,-45,-43,90,90,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,90,-76,90,90,90,90,-44,-46,90,-52,90,-54,-55,90,-27,-50,-47,90,90,90,-63,90,90,-51,]),'/':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[94,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,94,-42,94,94,94,94,-24,-25,-26,94,94,94,-71,-45,-43,94,94,94,94,94,-31,-32,-33,94,94,94,94,94,94,94,94,94,-76,94,94,94,94,-44,-46,94,-52,94,-54,-55,94,94,-50,94,94,94,94,-63,94,94,-51,]),'^':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[96,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,96,-42,96,96,96,96,-24,-25,-26,96,96,96,-71,-45,-43,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,-76,96,96,96,96,-44,-46,96,-52,96,-54,-55,96,96,-50,96,96,96,96,-63,96,96,-51,]),'<':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[97,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,97,-42,97,97,97,97,-24,-25,-26,97,97,97,-71,-45,-43,97,97,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,97,97,97,-76,97,97,97,97,-44,-46,97,-52,97,-54,-55,97,97,-50,97,97,97,97,-63,97,97,-51,]),'>':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[98,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,98,-42,98,98,98,98,-24,-25,-26,98,98,98,-71,-45,-43,98,98,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,98,98,98,-76,98,98,98,98,-44,-46,98,-52,98,-54,-55,98,98,-50,98,98,98,98,-63,98,98,-51,]),'EQUAL':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[99,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,99,-42,99,99,99,99,-24,-25,-26,99,99,99,-71,-45,-43,99,99,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,99,99,99,-76,99,99,99,99,-44,-46,99,-52,99,-54,-55,99,99,-50,99,99,99,99,-63,99,99,-51,]),'NOT_EQUAL':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[100,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,100,-42,100,100,100,100,-24,-25,-26,100,100,100,-71,-45,-43,100,100,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,100,100,100,-76,100,100,100,100,-44,-46,100,-52,100,-54,-55,100,100,-50,100,100,100,100,-63,100,100,-51,]),'GREATER_OR_EQUAL':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[101,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,101,-42,101,101,101,101,-24,-25,-26,101,101,101,-71,-45,-43,101,101,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,101,101,101,-76,101,101,101,101,-44,-46,101,-52,101,-54,-55,101,101,-50,101,101,101,101,-63,101,101,-51,]),'LESS_OR_EQUAL':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[102,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,102,-42,102,102,102,102,-24,-25,-26,102,102,102,-71,-45,-43,102,102,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,102,102,102,-76,102,102,102,102,-44,-46,102,-52,102,-54,-55,102,102,-50,102,102,102,102,-63,102,102,-51,]),'AND':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[103,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,103,-42,103,103,103,103,-24,-25,-26,103,103,103,-71,-45,-43,103,103,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,103,-76,103,103,103,103,-44,-46,103,-52,103,-54,-55,103,103,-50,103,103,103,103,-63,103,103,-51,]),'OR':([44,45,46,47,48,49,50,51,60,63,64,65,66,72,73,76,85,105,106,111,112,113,116,117,138,139,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,170,174,176,178,183,185,187,188,189,192,193,198,200,202,203,204,205,207,208,210,212,213,],[104,-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,104,-42,104,104,104,104,-24,-25,-26,104,104,104,-71,-45,-43,104,104,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,104,-76,104,104,104,104,-44,-46,104,-52,104,-54,-55,104,104,-50,104,104,104,104,-63,104,104,-51,]),',':([45,46,47,48,49,50,51,60,63,64,65,66,67,70,71,72,73,77,79,80,81,105,106,109,111,112,113,116,117,124,125,132,135,139,140,141,143,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,163,165,170,172,173,174,177,178,180,183,185,188,189,192,193,198,200,202,203,204,205,206,208,212,213,],[-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,121,125,-87,-88,-42,125,125,-94,-95,-68,-83,125,-24,-25,-26,-49,-62,173,-78,173,125,-71,125,-45,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-62,-76,173,121,-72,-86,-77,-89,-93,-96,173,-44,-46,-52,-82,-54,-55,-60,-27,-50,-47,-48,-73,125,-63,-61,-51,]),':':([45,46,47,48,49,50,51,60,63,64,65,66,105,106,111,112,113,116,117,139,141,143,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,183,185,187,188,192,193,198,200,202,203,204,208,212,213,],[-64,-65,-66,-67,-69,-70,-42,-53,-56,-57,-58,-59,-68,160,-24,-25,-26,-49,-62,-71,-45,-43,186,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-62,-76,-44,-46,201,-52,-54,-55,-60,-27,-50,-47,-48,-63,-61,-51,]),']':([45,46,47,48,49,50,51,52,60,63,64,65,66,105,106,108,109,110,111,112,113,116,117,125,139,141,143,144,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,163,173,183,185,187,188,189,192,193,198,200,202,203,204,208,210,212,213,],[-64,-65,-66,-67,-69,-70,-42,-3,-53,-56,-57,-58,-59,-68,-83,162,-79,-81,-24,-25,-26,-49,-62,-78,-71,-45,-43,185,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-62,-76,-80,-77,-44,-46,202,-52,-82,-54,-55,-60,-27,-50,-47,-48,-63,213,-61,-51,]),} @@ -88,41 +88,41 @@ ('constants -> NUMBER','constants',1,'p_constants','scad_parser.py',124), ('opt_else -> ','opt_else',0,'p_opt_else','scad_parser.py',127), ('opt_else -> ELSE expression','opt_else',2,'p_opt_else','scad_parser.py',128), - ('for_or_if -> for_loop expression','for_or_if',2,'p_for_or_if','scad_parser.py',131), - ('for_or_if -> IF ( expression ) expression opt_else','for_or_if',6,'p_for_or_if','scad_parser.py',132), - ('expression -> access_expr','expression',1,'p_expression','scad_parser.py',136), - ('expression -> logic_expr','expression',1,'p_expression','scad_parser.py',137), - ('expression -> list_stuff','expression',1,'p_expression','scad_parser.py',138), - ('expression -> assert_or_echo','expression',1,'p_expression','scad_parser.py',139), - ('expression -> assert_or_echo expression','expression',2,'p_expression','scad_parser.py',140), - ('expression -> constants','expression',1,'p_expression','scad_parser.py',141), - ('expression -> for_or_if','expression',1,'p_expression','scad_parser.py',142), - ('expression -> ( expression )','expression',3,'p_expression','scad_parser.py',143), - ('assignment_list -> ID = expression','assignment_list',3,'p_assignment_list','scad_parser.py',147), - ('assignment_list -> assignment_list , ID = expression','assignment_list',5,'p_assignment_list','scad_parser.py',148), - ('call -> ID ( call_parameter_list )','call',4,'p_call','scad_parser.py',152), - ('call -> ID ( )','call',3,'p_call','scad_parser.py',153), - ('tuple -> [ opt_expression_list ]','tuple',3,'p_tuple','scad_parser.py',156), - ('commas -> commas ,','commas',2,'p_commas','scad_parser.py',160), - ('commas -> ,','commas',1,'p_commas','scad_parser.py',161), - ('opt_expression_list -> expression_list','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',165), - ('opt_expression_list -> expression_list commas','opt_expression_list',2,'p_opt_expression_list','scad_parser.py',166), - ('opt_expression_list -> empty','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',167), - ('expression_list -> expression_list commas expression','expression_list',3,'p_expression_list','scad_parser.py',169), - ('expression_list -> expression','expression_list',1,'p_expression_list','scad_parser.py',170), - ('opt_call_parameter_list -> ','opt_call_parameter_list',0,'p_opt_call_parameter_list','scad_parser.py',174), - ('opt_call_parameter_list -> call_parameter_list','opt_call_parameter_list',1,'p_opt_call_parameter_list','scad_parser.py',175), - ('call_parameter_list -> call_parameter_list commas call_parameter','call_parameter_list',3,'p_call_parameter_list','scad_parser.py',178), - ('call_parameter_list -> call_parameter','call_parameter_list',1,'p_call_parameter_list','scad_parser.py',179), - ('call_parameter -> expression','call_parameter',1,'p_call_parameter','scad_parser.py',182), - ('call_parameter -> ID = expression','call_parameter',3,'p_call_parameter','scad_parser.py',183), - ('opt_parameter_list -> parameter_list','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',186), - ('opt_parameter_list -> parameter_list commas','opt_parameter_list',2,'p_opt_parameter_list','scad_parser.py',187), - ('opt_parameter_list -> empty','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',188), - ('parameter_list -> parameter_list commas parameter','parameter_list',3,'p_parameter_list','scad_parser.py',196), - ('parameter_list -> parameter','parameter_list',1,'p_parameter_list','scad_parser.py',197), - ('parameter -> ID','parameter',1,'p_parameter','scad_parser.py',204), - ('parameter -> ID = expression','parameter',3,'p_parameter','scad_parser.py',205), - ('function -> FUNCTION ID ( opt_parameter_list ) = expression','function',7,'p_function','scad_parser.py',209), - ('module -> MODULE ID ( opt_parameter_list ) statement','module',6,'p_module','scad_parser.py',218), + ('for_or_if -> for_loop expression','for_or_if',2,'p_for_or_if','scad_parser.py',132), + ('for_or_if -> IF ( expression ) expression opt_else','for_or_if',6,'p_for_or_if','scad_parser.py',133), + ('expression -> access_expr','expression',1,'p_expression','scad_parser.py',137), + ('expression -> logic_expr','expression',1,'p_expression','scad_parser.py',138), + ('expression -> list_stuff','expression',1,'p_expression','scad_parser.py',139), + ('expression -> assert_or_echo','expression',1,'p_expression','scad_parser.py',140), + ('expression -> assert_or_echo expression','expression',2,'p_expression','scad_parser.py',141), + ('expression -> constants','expression',1,'p_expression','scad_parser.py',142), + ('expression -> for_or_if','expression',1,'p_expression','scad_parser.py',143), + ('expression -> ( expression )','expression',3,'p_expression','scad_parser.py',144), + ('assignment_list -> ID = expression','assignment_list',3,'p_assignment_list','scad_parser.py',149), + ('assignment_list -> assignment_list , ID = expression','assignment_list',5,'p_assignment_list','scad_parser.py',150), + ('call -> ID ( call_parameter_list )','call',4,'p_call','scad_parser.py',154), + ('call -> ID ( )','call',3,'p_call','scad_parser.py',155), + ('tuple -> [ opt_expression_list ]','tuple',3,'p_tuple','scad_parser.py',158), + ('commas -> commas ,','commas',2,'p_commas','scad_parser.py',162), + ('commas -> ,','commas',1,'p_commas','scad_parser.py',163), + ('opt_expression_list -> expression_list','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',167), + ('opt_expression_list -> expression_list commas','opt_expression_list',2,'p_opt_expression_list','scad_parser.py',168), + ('opt_expression_list -> empty','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',169), + ('expression_list -> expression_list commas expression','expression_list',3,'p_expression_list','scad_parser.py',171), + ('expression_list -> expression','expression_list',1,'p_expression_list','scad_parser.py',172), + ('opt_call_parameter_list -> ','opt_call_parameter_list',0,'p_opt_call_parameter_list','scad_parser.py',176), + ('opt_call_parameter_list -> call_parameter_list','opt_call_parameter_list',1,'p_opt_call_parameter_list','scad_parser.py',177), + ('call_parameter_list -> call_parameter_list commas call_parameter','call_parameter_list',3,'p_call_parameter_list','scad_parser.py',180), + ('call_parameter_list -> call_parameter','call_parameter_list',1,'p_call_parameter_list','scad_parser.py',181), + ('call_parameter -> expression','call_parameter',1,'p_call_parameter','scad_parser.py',184), + ('call_parameter -> ID = expression','call_parameter',3,'p_call_parameter','scad_parser.py',185), + ('opt_parameter_list -> parameter_list','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',188), + ('opt_parameter_list -> parameter_list commas','opt_parameter_list',2,'p_opt_parameter_list','scad_parser.py',189), + ('opt_parameter_list -> empty','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',190), + ('parameter_list -> parameter_list commas parameter','parameter_list',3,'p_parameter_list','scad_parser.py',198), + ('parameter_list -> parameter','parameter_list',1,'p_parameter_list','scad_parser.py',199), + ('parameter -> ID','parameter',1,'p_parameter','scad_parser.py',206), + ('parameter -> ID = expression','parameter',3,'p_parameter','scad_parser.py',207), + ('function -> FUNCTION ID ( opt_parameter_list ) = expression','function',7,'p_function','scad_parser.py',211), + ('module -> MODULE ID ( opt_parameter_list ) statement','module',6,'p_module','scad_parser.py',220), ] diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index 182048f5..f96e3b4e 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -52,8 +52,8 @@ t_GREATER_OR_EQUAL = ">=" t_LESS_OR_EQUAL = "<=" t_NOT_EQUAL = "!=" -t_AND = "\&\&" -t_OR = "\|\|" +t_AND = r"\&\&" +t_OR = r"\|\|" t_FILENAME = r'<[a-zA-Z_0-9/\\\.-]*>' diff --git a/solid/solidpython.py b/solid/solidpython.py index 4fc935f2..71bd167a 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -1,8 +1,8 @@ #! /usr/bin/env python -# Simple Python OpenSCAD Code Generator +# Python OpenSCAD Code Generator # Copyright (C) 2009 Philipp Tiefenbacher -# Amendments & additions, (C) 2011-2019 Evan Jones +# Amendments & additions, (C) 2011-2021 Evan Jones # # License: LGPL 2.1 or later # @@ -26,6 +26,8 @@ import pkg_resources import re +from solid.customizer import CompoundFloat, Customizer + PathStr = Union[Path, str] AnimFunc = Callable[[Optional[float]], 'OpenSCADObject'] # These are features added to SolidPython but NOT in OpenSCAD. @@ -412,22 +414,56 @@ def _find_include_strings(obj: Union[IncludedOpenSCADObject, OpenSCADObject]) -> include_strings.update(_find_include_strings(param)) return include_strings +def _find_customizer_inits(scad_object:OpenSCADObject) -> Set[str]: + # Recursively travel scad_object and all its children, making a set + # of all unique Customizer instances we find. + # Return a set of all OpenSCAD customizer initializer strings, + # which will then define the OpenSCAD Customizer GUI. + + customizer_strings = set() + for child in scad_object.children: + customizer_strings.update(_find_customizer_inits(child)) + # We accept Customizer instances as parameters to functions, + # so search in scad_object.params as well + for param in scad_object.params.values(): + if isinstance(param, Customizer): + customizer_strings.add(param.scad_declaration()) + # params may be CompoundFloats that contain Customizer instances, like: + # (2*slider) or (2*((slider * slider) + 3)) + # Recurse each CompoundFloat for any Customizer instances + elif isinstance(param, CompoundFloat): + customizers = param.customizer_instances() + customizer_strings.update((c.scad_declaration() for c in customizers)) + + elif hasattr(param, '__iter__') and not isinstance(param, (str, bytes)): + for elt in param: + if isinstance(elt, Customizer): + customizer_strings.add(elt.scad_declaration()) + return customizer_strings + def scad_render(scad_object: OpenSCADObject, file_header: str = '') -> str: # Make this object the root of the tree root = scad_object # Scan the tree for all instances of # IncludedOpenSCADObject, storing their strings + # and render the strings include_strings = _find_include_strings(root) - - # and render the string includes = ''.join(include_strings) + "\n" + + # Similarly, find all instances of Customizer subclasses and render them + # at the file's top level to create a Customizer GUI + customizer_declarations = '' + customizer_inits = _find_customizer_inits(root) + if customizer_inits: + customizer_declarations = ''.join( sorted(list(customizer_inits))) + '\n' + scad_body = root._render() if file_header and not file_header.endswith('\n'): file_header += '\n' - return file_header + includes + scad_body + return file_header + includes + customizer_declarations + scad_body def scad_render_animated(func_to_animate: AnimFunc, steps: int =20, @@ -759,6 +795,8 @@ def _unsubbed_keyword(subbed_keyword: str) -> str: from . import objects def py2openscad(o: Union[bool, float, str, Iterable]) -> str: + if isinstance(o, Customizer): + return str(o) if type(o) == bool: return str(o).lower() if type(o) == float: diff --git a/solid/test/ExpandedTestCase.py b/solid/test/ExpandedTestCase.py deleted file mode 100644 index cba00a43..00000000 --- a/solid/test/ExpandedTestCase.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -A version of unittest that gives output in an easier to use format -""" -import sys -import unittest -import difflib - - -class DiffOutput(unittest.TestCase): - - def assertEqual(self, first, second, msg=None): - """ - Override assertEqual and print(a context diff if msg=None) - """ - # Test if both are strings, in Python 2 & 3 - string_types = str if sys.version_info[0] == 3 else basestring - - if isinstance(first, string_types) and isinstance(second, string_types): - if not msg: - msg = 'Strings are not equal:\n' + ''.join( - difflib.unified_diff( - [first], - [second], - fromfile='actual', - tofile='expected' - ) - ) - return super(DiffOutput, self).assertEqual(first, second, msg=msg) diff --git a/solid/test/solidpython_testcase.py b/solid/test/solidpython_testcase.py new file mode 100644 index 00000000..404f4490 --- /dev/null +++ b/solid/test/solidpython_testcase.py @@ -0,0 +1,39 @@ +""" +A version of unittest that gives output in an easier to use format +""" +import sys +import unittest +import difflib +import re +from typing import Union +from solid import OpenSCADObject, scad_render + +class SolidPythonTestCase(unittest.TestCase): + + def assertEqual(self, first, second, msg=None): + """ + Override assertEqual and print(a context diff if msg=None) + """ + if isinstance(first, str) and isinstance(second, str): + if not msg: + msg = 'Strings are not equal:\n' + ''.join( + difflib.unified_diff( + [first], + [second], + fromfile='actual', + tofile='expected' + ) + ) + return super(SolidPythonTestCase, self).assertEqual(first, second, msg=msg) + + def assertEqualNoWhitespace(self, a, b): + remove_whitespace = lambda s: re.subn(r'[\s\n]','', s)[0] + self.assertEqual(remove_whitespace(a), remove_whitespace(b)) + + def assertEqualOpenScadObject(self, expected:str, actual:Union[OpenSCADObject, str]): + if isinstance(actual, OpenSCADObject): + act = scad_render(actual) + elif isinstance(actual, str): + act = actual + self.assertEqualNoWhitespace(expected, act) + diff --git a/solid/test/test_customizer.py b/solid/test/test_customizer.py new file mode 100755 index 00000000..d9026850 --- /dev/null +++ b/solid/test/test_customizer.py @@ -0,0 +1,187 @@ +#! /usr/bin/env python3 +import unittest + + +from solid import * +from solid.test.solidpython_testcase import SolidPythonTestCase +from solid.customizer import (CustomizerCheckbox, CustomizerDropdownString, + CustomizerSlider, CustomizerDropdownNumber, CustomizerSpinbox, CustomizerTextbox, + CompoundFloat) + +class TestCompoundFloat(SolidPythonTestCase): + def setUp(self): + self.cf = CompoundFloat(4.0) + self.slider = CustomizerSlider('slider', 3) + + def testSingleFloat(self): + a = CompoundFloat(4.0) + expected = '(4.0)' + actual = str(a) + self.assertEqual(expected, actual) + + def testSingleInt(self): + # NOTE CompoundFloat(4.0) has a different string value than CompoundFloat(4) + # Is this OK? + a = CompoundFloat(4) + expected = '(4)' + actual = str(a) + self.assertEqual(expected, actual) + + def testSimpleAdd(self): + b = CompoundFloat(self.cf, float.__add__, 2.0) + expected = '((4.0) + 2.0)' + actual = str(b) + self.assertEqual(expected, actual) + + def testInfixAdd(self): + # Confirm that infix operators work with CompoundFloats + c = self.cf + 3 + expected = '((4.0) + 3)' + actual = str(c) + self.assertEqual(expected, actual) + + def testInfixCommutative(self): + # confirm that infix operators are commutative & stay CompoundFloats + d = 3 + self.cf + expected = '((4.0) + 3)' + actual = str(d) + self.assertEqual(expected, actual) + + def testCustomizerSubclasses(self): + # Confirm that CompoundFloat play nicely with Customizer subclasses + e = CompoundFloat(self.slider, float.__add__, self.cf) + expected = '(slider + (4.0))' + actual = str(e) + self.assertEqual(expected, actual) + + def testCustomizerInfix(self): + # Confirm that infix operators work with Customizer subclasses + f = self.slider + 3 + expected = '(slider + 3)' + actual = str(f) + self.assertEqual(expected, actual) + + def testUnaryNegative(self): + # Confirm unary operations work + g = -self.cf + expected = '(-1.0 * (4.0))' + actual = str(g) + self.assertEqual(expected, actual) + + def testUnaryPositive(self): + h = +self.cf + expected = '(4.0)' + actual = str(h) + self.assertEqual(expected, actual) + + # Confirm non-commutative operations work + def testRsub(self): + i = 1 - self.cf + expected = '(1.0 - (4.0))' + actual = str(i) + self.assertEqual(expected, actual) + + j = 1 - self.slider + actual = str(j) + expected = '(1.0 - slider)' + self.assertEqual(expected, actual) + + def testDiv(self): + j = self.cf/2 + expected = '((4.0) / 2)' + actual = str(j) + self.assertEqual(expected, actual) + + k = self.slider / 2 + expected = '(slider / 2)' + actual = str(k) + self.assertEqual(expected, actual) + + def testRdiv(self): + k = 2 / self.cf + expected = '(2.0 / (4.0))' + actual = str(k) + self.assertEqual(expected, actual) + + l = 2 / self.slider + expected = '(2.0 / slider)' + actual = str(l) + self.assertEqual(expected, actual) + + +class TestCustomizer(SolidPythonTestCase): + def testCustomizerCheckbox(self): + cb = CustomizerCheckbox('cb', True) + actual = cube(5, center=cb) + expected = 'cb = true; cube(center=cb, size=5);' + self.assertEqualOpenScadObject(expected, actual) + + def testCustomizerSlider(self): + cs1 = CustomizerSlider('cs1', 0.5) + actual = cube(cs1) + expected = 'cs1 = 0.5; // [0:1] cube(size=cs1);' + self.assertEqualOpenScadObject(expected, actual) + + # Make sure math involving customizer values work + actual = cube([cs1, 2*cs1, cs1 + cs1]) + expected = 'cs1 = 0.5; // [0:1] cube(size=[cs1, (cs1*2), (cs1+cs1)]);' + self.assertEqualOpenScadObject(expected, actual) + + # FIXME: tests for min, max, step + + def testCustomizerDropdownNumber(self): + cdn = CustomizerDropdownNumber('cdn', 2, [1,2,3]) + actual = cube(cdn) + expected = 'cdn = 2; // [1,2,3] cube(size=cdn);' + self.assertEqualOpenScadObject(expected, actual) + + # Make sure math involving customizer values work + actual = cube(2*cdn) + expected = 'cdn = 2; // [1,2,3] cube(size=(cdn*2));' + self.assertEqualOpenScadObject(expected, actual) + + # Make sure labeled dropdowns work + cdn2 = CustomizerDropdownNumber('cdn2', 2, {2:'a', 3:'b', 4.5:'c'}) + actual = cube(cdn2) + expected = 'cdn2 = 2; //[2:a,3:b,4.5:c] cube(size=cdn2);' + self.assertEqualOpenScadObject(expected, actual) + + # And math on labeled dropdowns + actual = cube(2 * cdn2) + expected = 'cdn2 = 2; //[2:a,3:b,4.5:c] cube(size=(cdn2*2));' + self.assertEqualOpenScadObject(expected, actual) + + def testCustomizerSpinbox(self): + csb = CustomizerSpinbox('csb', 5) + actual = cube(csb) + expected = 'csb=5; cube(size=csb);' + self.assertEqualOpenScadObject(expected, actual) + + # NOTE: we do negative numbers & subtraction in ugly fashion. + # This lets us do subtraction & division in terms of addition and + # multiplication, which are commutative. Maybe there's a more graceful way? + actual = cube(1-csb) + expected = 'csb=5; cube(size=(1.0-csb));' + self.assertEqualOpenScadObject(expected, actual) + + def testCustomizerDropdownString(self): + cds = CustomizerDropdownString('cds', 'foo', ['foo', 'bar', 'baz']) + actual = text(cds) + expected = 'cds="foo";//[foo,bar,baz] text(text=cds);' + self.assertEqualOpenScadObject(expected, actual) + + # Test labeled string options + cds2 = CustomizerDropdownString('cds2', 'S', {'S':'Small', 'M':'Medium', 'L':'Large'}) + actual = text(cds2) + expected = 'cds2="S"; // [S:Small,M:Medium,L:Large] text(text=cds2);' + self.assertEqualOpenScadObject(expected, actual) + + def testCustomizerTextbox(self): + ctb = CustomizerTextbox('ctb', 'hello') + actual = text(ctb) + expected = 'ctb="hello"; text(text=ctb);' + self.assertEqualOpenScadObject(expected, actual) + + +if __name__ == '__main__': + unittest.main() diff --git a/solid/test/test_screw_thread.py b/solid/test/test_screw_thread.py index 3860b931..1fbb590b 100755 --- a/solid/test/test_screw_thread.py +++ b/solid/test/test_screw_thread.py @@ -4,11 +4,11 @@ from solid.screw_thread import default_thread_section, thread from solid.solidpython import scad_render -from solid.test.ExpandedTestCase import DiffOutput +from solid.test.solidpython_testcase import SolidPythonTestCase SEGMENTS = 4 -class TestScrewThread(DiffOutput): +class TestScrewThread(SolidPythonTestCase): def setUp(self): self.tooth_height = 10 self.tooth_depth = 5 diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 08f6af32..42aa2b00 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -13,7 +13,7 @@ from solid.objects import scale, surface, union from solid.solidpython import scad_render, scad_render_animated_file, scad_render_to_file -from solid.test.ExpandedTestCase import DiffOutput +from solid.test.solidpython_testcase import SolidPythonTestCase scad_test_case_templates = [ {'name': 'polygon', 'class': 'polygon' , 'kwargs': {'paths': [[0, 1, 2]]}, 'expected': '\n\npolygon(paths = [[0, 1, 2]], points = [[0, 0], [1, 0], [0, 1]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]]}, }, @@ -84,7 +84,7 @@ def _cleanup(self): pass -class TestSolidPython(DiffOutput): +class TestSolidPython(SolidPythonTestCase): # test cases will be dynamically added to this instance def expand_scad_path(self, filename): @@ -95,22 +95,22 @@ def test_infix_union(self): a = cube(2) b = sphere(2) expected = '\n\nunion() {\n\tcube(size = 2);\n\tsphere(r = 2);\n}' - actual = scad_render(a + b) - self.assertEqual(expected, actual) + actual = a + b + self.assertEqualOpenScadObject(expected, actual) def test_infix_difference(self): a = cube(2) b = sphere(2) expected = '\n\ndifference() {\n\tcube(size = 2);\n\tsphere(r = 2);\n}' - actual = scad_render(a - b) - self.assertEqual(expected, actual) + actual = a - b + self.assertEqualOpenScadObject(expected, actual) def test_infix_intersection(self): a = cube(2) b = sphere(2) expected = '\n\nintersection() {\n\tcube(size = 2);\n\tsphere(r = 2);\n}' - actual = scad_render(a * b) - self.assertEqual(expected, actual) + actual = a * b + self.assertEqualOpenScadObject(expected, actual) def test_parse_scad_callables(self): test_str = """ @@ -228,9 +228,14 @@ def test_import_scad(self): self.assertRaises(ValueError, import_scad, 'path/doesnt/exist.scad') # Test that we recursively import directories correctly - examples = import_scad(include_file.parent) - self.assertTrue(hasattr(examples, 'scad_to_include')) - self.assertTrue(hasattr(examples.scad_to_include, 'steps')) + # write to a temp directory to make sure it only contains what we want + with tempfile.TemporaryDirectory() as tmpdirname: + import shutil + dest_file = Path(tmpdirname) / include_file.name + shutil.copyfile(include_file, dest_file) + tmpdir_namespace = import_scad(tmpdirname) + self.assertTrue(hasattr(tmpdir_namespace, 'scad_to_include')) + self.assertTrue(hasattr(tmpdir_namespace.scad_to_include, 'steps')) # TODO: we should test that: # A) scad files in the designated OpenSCAD library directories @@ -292,36 +297,35 @@ def test_include(self): def test_extra_args_to_included_scad(self): include_file = self.expand_scad_path("examples/scad_to_include.scad") mod = import_scad(include_file) - a = mod.steps(3, external_var=True) - actual = scad_render(a) + actual = mod.steps(3, external_var=True) - abs_path = a._get_include_path(include_file) + abs_path = actual._get_include_path(include_file) expected = f"use <{abs_path}>\n\n\nsteps(external_var = true, howmany = 3);" - self.assertEqual(expected, actual) + self.assertEqualOpenScadObject(expected, actual) def test_background(self): a = cube(10) expected = '\n\n%cube(size = 10);' - actual = scad_render(background(a)) - self.assertEqual(expected, actual) + actual = background(a) + self.assertEqualOpenScadObject(expected, actual) def test_debug(self): a = cube(10) expected = '\n\n#cube(size = 10);' - actual = scad_render(debug(a)) - self.assertEqual(expected, actual) + actual = debug(a) + self.assertEqualOpenScadObject(expected, actual) def test_disable(self): a = cube(10) expected = '\n\n*cube(size = 10);' - actual = scad_render(disable(a)) - self.assertEqual(expected, actual) + actual = disable(a) + self.assertEqualOpenScadObject(expected, actual) def test_root(self): a = cube(10) expected = '\n\n!cube(size = 10);' - actual = scad_render(root(a)) - self.assertEqual(expected, actual) + actual = root(a) + self.assertEqualOpenScadObject(expected, actual) def test_color(self): all_args = [ @@ -339,14 +343,14 @@ def test_color(self): ] for args, expected in zip(all_args, expecteds): col = color(**args) - actual = scad_render(col) - self.assertEqual(expected, actual) + actual = col + self.assertEqualOpenScadObject(expected, actual) def test_explicit_hole(self): a = cube(10, center=True) + hole()(cylinder(2, 20, center=True)) expected = '\n\ndifference(){\n\tunion() {\n\t\tcube(center = true, size = 10);\n\t}\n\t/* Holes Below*/\n\tunion(){\n\t\tcylinder(center = true, h = 20, r = 2);\n\t} /* End Holes */ \n}' - actual = scad_render(a) - self.assertEqual(expected, actual) + actual = a + self.assertEqualOpenScadObject(expected, actual) def test_hole_transform_propagation(self): # earlier versions of holes had problems where a hole @@ -364,8 +368,8 @@ def test_hole_transform_propagation(self): a = cube(10, center=True) + h + h_vert expected = '\n\ndifference(){\n\tunion() {\n\t\tcube(center = true, size = 10);\n\t\trotate(a = -90, v = [0, 1, 0]) {\n\t\t}\n\t}\n\t/* Holes Below*/\n\tunion(){\n\t\trotate(a = 90, v = [0, 1, 0]) {\n\t\t\tcylinder(center = true, h = 20, r = 2);\n\t\t}\n\t\trotate(a = -90, v = [0, 1, 0]){\n\t\t\trotate(a = 90, v = [0, 1, 0]) {\n\t\t\t\tcylinder(center = true, h = 20, r = 2);\n\t\t\t}\n\t\t}\n\t} /* End Holes */ \n}' - actual = scad_render(a) - self.assertEqual(expected, actual) + actual = a + self.assertEqualOpenScadObject(expected, actual) def test_separate_part_hole(self): # Make two parts, a block with hole, and a cylinder that @@ -389,8 +393,8 @@ def test_separate_part_hole(self): a = p1 + p2 expected = '\n\nunion() {\n\tdifference(){\n\t\tdifference() {\n\t\t\tcube(center = true, size = 10);\n\t\t}\n\t\t/* Holes Below*/\n\t\tunion(){\n\t\t\tcylinder(center = true, h = 12, r = 2);\n\t\t} /* End Holes */ \n\t}\n\tcylinder(center = true, h = 14, r = 1.5000000000);\n}' - actual = scad_render(a) - self.assertEqual(expected, actual) + actual = a + self.assertEqualOpenScadObject(expected, actual) def test_scad_render_animated_file(self): def my_animate(_time=0): @@ -457,8 +461,8 @@ def test_numpy_type(self): import numpy # type: ignore numpy_cube = cube(size=numpy.array([1, 2, 3])) expected = '\n\ncube(size = [1,2,3]);' - actual = scad_render(numpy_cube) - self.assertEqual(expected, actual, 'Numpy SolidPython not rendered correctly') + actual = numpy_cube + self.assertEqualOpenScadObject(expected, actual) except ImportError: pass @@ -479,8 +483,8 @@ def __iter__(self): for iterable in iterables: name = type(iterable).__name__ - actual = scad_render(cube(size=iterable)) - self.assertEqual(expected, actual, f'{name} SolidPython not rendered correctly') + actual = cube(size=iterable) + self.assertEqualOpenScadObject(expected, actual) def single_test(test_dict): @@ -495,9 +499,8 @@ def test(self): call_str += ')' scad_obj = eval(call_str) - actual = scad_render(scad_obj) - - self.assertEqual(expected, actual) + actual = scad_obj + self.assertEqualOpenScadObject(expected, actual) return test diff --git a/solid/test/test_splines.py b/solid/test/test_splines.py index 10641044..3cef5c96 100755 --- a/solid/test/test_splines.py +++ b/solid/test/test_splines.py @@ -1,7 +1,7 @@ #! /usr/bin/env python import unittest -from solid.test.ExpandedTestCase import DiffOutput +from solid.test.solidpython_testcase import SolidPythonTestCase from solid import * from solid.utils import euclidify from solid.splines import catmull_rom_points, catmull_rom_prism, bezier_points, bezier_polygon @@ -10,7 +10,7 @@ SEGMENTS = 8 -class TestSplines(DiffOutput): +class TestSplines(SolidPythonTestCase): def setUp(self): self.points = [ Point3(0,0), diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 8ba3c2de..ce3cb47a 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -7,7 +7,7 @@ from solid import scad_render from solid.objects import cube, polygon, sphere, translate -from solid.test.ExpandedTestCase import DiffOutput +from solid.test.solidpython_testcase import SolidPythonTestCase from solid.utils import BoundingBox, arc, arc_inverted, euc_to_arr, euclidify from solid.utils import extrude_along_path, fillet_2d, is_scad, offset_points from solid.utils import split_body_planar, transform_to_point, project_to_2D @@ -56,20 +56,9 @@ ] -class TestSPUtils(DiffOutput): +class TestSPUtils(SolidPythonTestCase): # Test cases will be dynamically added to this instance # using the test case arrays above - def assertEqualNoWhitespace(self, a, b): - remove_whitespace = lambda s: re.subn(r'[\s\n]','', s)[0] - self.assertEqual(remove_whitespace(a), remove_whitespace(b)) - - def assertEqualOpenScadObject(self, expected:str, actual:Union[OpenSCADObject, str]): - if isinstance(actual, OpenSCADObject): - act = scad_render(actual) - elif isinstance(actual, str): - act = actual - self.assertEqualNoWhitespace(expected, act) - def test_split_body_planar(self): offset = [10, 10, 10] body = translate(offset)(sphere(20))