From 797097ec57645d303f2368ab7dba42d3b3a8628a Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sun, 22 Dec 2019 14:16:20 +0300 Subject: [PATCH 01/90] Adds proper code highlighting --- README.rst | 65 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/README.rst b/README.rst index b8ad49fb..32c5ecf6 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ simple example: This Python code: -:: +.. code:: python from solid import * d = difference()( @@ -57,7 +57,7 @@ This Python code: Generates this OpenSCAD code: -:: +.. code:: python difference(){ cube(10); @@ -67,7 +67,7 @@ Generates this OpenSCAD code: That doesn't seem like such a savings, but the following SolidPython code is a lot shorter (and I think clearer) than the SCAD code it compiles to: -:: +.. code:: python from solid import * from solid.utils import * @@ -75,7 +75,7 @@ code is a lot shorter (and I think clearer) than the SCAD code it compiles to: Generates this OpenSCAD code: -:: +.. code:: difference(){ union(){ @@ -104,7 +104,7 @@ Installing SolidPython - Install latest release via `PyPI `__: - :: + .. code:: bash pip install solidpython @@ -115,19 +115,19 @@ Installing SolidPython - Install current master straight from Github: - :: + .. code:: bash - pip install git+https://github.com/SolidCode/SolidPython.git + pip install git+https://github.com/SolidCode/SolidPython.git Using SolidPython ================= - Include SolidPython at the top of your Python file: - :: + .. code:: python - from solid import * - from solid.utils import * # Not required, but the utils module is useful + from solid import * + from solid.utils import * # Not required, but the utils module is useful (See `this issue `__ for a discussion of other import styles @@ -137,7 +137,7 @@ Using SolidPython **OpenSCAD:** - :: + .. code:: difference(){ cube(10); @@ -146,7 +146,7 @@ Using SolidPython **SolidPython:** - :: + .. code:: d = difference()( cube(10), # Note the comma between each element! @@ -166,19 +166,22 @@ Using SolidPython Importing OpenSCAD code ======================= + - Use ``solid.import_scad(path)`` to import OpenSCAD code. **Ex:** ``scadfile.scad`` -:: + +.. code:: module box(w,h,d){ cube([w,h,d]); } ``your_file.py`` -:: + +.. code:: python from solid import * @@ -188,7 +191,7 @@ Importing OpenSCAD code - Recursively import OpenSCAD code by calling ``import_scad()`` with a directory argument. -:: +.. code:: python from solid import * @@ -203,15 +206,18 @@ Importing OpenSCAD code - OpenSCAD has the ``use()`` and ``include()`` statements for importing SCAD code, and SolidPython has them, too. They pollute the global namespace, though, and you may have better luck with ``import_scad()``, **Ex:** + ``scadfile.scad`` -:: + +.. code:: module box(w,h,d){ cube([w,h,d]); } ``your_file.py`` -:: + +.. code:: python from solid import * @@ -228,7 +234,7 @@ The best way to learn how SolidPython works is to look at the included example code. If you've installed SolidPython, the following line of Python will print(the location of ) the examples directory: -:: +.. code:: python import os, solid; print(os.path.dirname(solid.__file__) + '/examples') @@ -249,13 +255,13 @@ Basic operators Following Elmo Mäntynen's suggestion, SCAD objects override the basic operators + (union), - (difference), and \* (intersection). So -:: +.. code:: python c = cylinder(r=10, h=5) + cylinder(r=2, h=30) is the same as: -:: +.. code:: python c = union()( cylinder(r=10, h=5), @@ -264,14 +270,14 @@ is the same as: Likewise: -:: +.. code:: python c = cylinder(r=10, h=5) c -= cylinder(r=2, h=30) is the same as: -:: +.. code:: python c = difference()( cylinder(r=10, h=5), @@ -296,7 +302,7 @@ structure. Example: -:: +.. code:: python outer = cylinder(r=pipe_od, h=seg_length) inner = cylinder(r=pipe_id, h=seg_length) @@ -332,7 +338,7 @@ Currently these include: Directions: (up, down, left, right, forward, back) for arranging things: ------------------------------------------------------------------------ -:: +.. code:: python up(10)( cylinder() @@ -340,7 +346,7 @@ Directions: (up, down, left, right, forward, back) for arranging things: seems a lot clearer to me than: -:: +.. code:: python translate( [0,0,10])( cylinder() @@ -355,13 +361,13 @@ Arcs I've found this useful for fillets and rounds. -:: +.. code:: python arc(rad=10, start_degrees=90, end_degrees=210) draws an arc of radius 10 counterclockwise from 90 to 210 degrees. -:: +.. code:: python arc_inverted(rad=10, start_degrees=0, end_degrees=90) @@ -384,7 +390,7 @@ Basic color library You can change an object's color by using the OpenSCAD ``color([rgba_array])`` function: -:: +.. code:: python transparent_blue = color([0,0,1, 0.5])(cube(10)) # Specify with RGB[A] red_obj = color(Red)(cube(10)) # Or use predefined colors @@ -436,7 +442,8 @@ Jupyter Renderer ---------------- Render SolidPython or OpenSCAD code in Jupyter notebooks using `ViewSCAD `__, or install directly via: -:: + +.. code:: bash pip install viewscad From d4916775a7226f666572c3926f574650ac6d812e Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 31 Dec 2019 14:48:51 -0600 Subject: [PATCH 02/90] Fixed bug in patch_euclid.set_length(); it was only operating on x & y, but not z. How didn't this break anything years ago? Also, patched Vector2 class as well as Vector3 --- solid/patch_euclid.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/solid/patch_euclid.py b/solid/patch_euclid.py index 61bef8c7..68c30f1d 100644 --- a/solid/patch_euclid.py +++ b/solid/patch_euclid.py @@ -1,18 +1,31 @@ import euclid3 -from euclid3 import * +from euclid3 import Vector3, Vector2, Line3 # NOTE: The PyEuclid on PyPi doesn't include several elements added to # the module as of 13 Feb 2013. Add them here until euclid supports them -def as_arr_local(self): +def as_arr_local2(self): + return [self.x, self.y] + +def as_arr_local3(self): return [self.x, self.y, self.z] -def set_length_local(self, length): +def set_length_local2(self, length): + d = self.magnitude() + if d: + factor = length / d + self.x *= factor + self.y *= factor + + return self + +def set_length_local3(self, length): d = self.magnitude() if d: factor = length / d self.x *= factor self.y *= factor + self.z *= factor return self @@ -30,8 +43,14 @@ def _intersect_line3_line3(A, B): def run_euclid_patch(): if 'as_arr' not in dir(Vector3): - Vector3.as_arr = as_arr_local + Vector3.as_arr = as_arr_local3 + if 'as_arr' not in dir(Vector2): + Vector2.as_arr = as_arr_local2 + if 'set_length' not in dir(Vector3): - Vector3.set_length = set_length_local + Vector3.set_length = set_length_local3 + if 'set_length' not in dir(Vector2): + Vector2.set_length = set_length_local2 + if '_intersect_line3' not in dir(Line3): Line3._intersect_line3 = _intersect_line3_line3 From acc3cbdb8303bc5d57a9d115a619d0ef7680ffce Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 31 Dec 2019 14:49:59 -0600 Subject: [PATCH 03/90] ignore a scratch/ directory for development --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2c7489ab..26fc47d6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ parts lib lib64 Doc/_build +scratch/ \ No newline at end of file From a48cf1d0f6b85c5e649a8bb996f9a400e537ee50 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Thu, 2 Jan 2020 14:54:58 -0600 Subject: [PATCH 04/90] control_points() accepts a color argument --- solid/splines.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solid/splines.py b/solid/splines.py index 02878c67..e0711425 100644 --- a/solid/splines.py +++ b/solid/splines.py @@ -2,7 +2,7 @@ from math import pow from solid import circle, cylinder, polygon, color, OpenSCADObject, translate, linear_extrude -from solid.utils import bounding_box, right, Red +from solid.utils import bounding_box, right, Red, Tuple3 from euclid3 import Vector2, Vector3, Point2, Point3 from typing import Sequence, Tuple, Union, List, cast @@ -182,7 +182,7 @@ def _bez33(u:float) -> float: # =========== # = HELPERS = # =========== -def control_points(points: Sequence[Point23], extrude_height:float=0, center:bool=True) -> OpenSCADObject: +def control_points(points: Sequence[Point23], extrude_height:float=0, center:bool=True, points_color:Tuple3=Red) -> OpenSCADObject: """ Return a list of red cylinders/circles (depending on `extrude_height`) at a supplied set of 2D points. Useful for visualizing and tweaking a curve's @@ -198,5 +198,5 @@ def control_points(points: Sequence[Point23], extrude_height:float=0, center:boo else: h = extrude_height * 1.1 c = cylinder(r=r, h=h, center=center) - controls = color(Red)([translate([p.x, p.y])(c) for p in points]) + controls = color(points_color)([translate([p.x, p.y])(c) for p in points]) return controls From 10418a0f110157a532c63a37eb24392b4e8c12c1 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Thu, 2 Jan 2020 14:56:36 -0600 Subject: [PATCH 05/90] condense assertEqualNoWhitespace() so it's self-contained --- solid/test/test_screw_thread.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/solid/test/test_screw_thread.py b/solid/test/test_screw_thread.py index 5a09320d..25a2147a 100755 --- a/solid/test/test_screw_thread.py +++ b/solid/test/test_screw_thread.py @@ -8,9 +8,6 @@ SEGMENTS = 4 -def remove_whitespace(a_str): - return re.subn(r'[\s\n]','', a_str)[0] - class TestScrewThread(DiffOutput): def setUp(self): self.tooth_height = 10 @@ -18,6 +15,7 @@ def setUp(self): self.outline = default_thread_section(tooth_height=self.tooth_height, tooth_depth=self.tooth_depth) 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 test_thread(self): @@ -71,7 +69,7 @@ def test_conical_thread_external(self): neck_in_degrees=45, neck_out_degrees=45, external=True) - actual = remove_whitespace(scad_render(actual_obj)) + actual = scad_render(actual_obj) expected = '''intersection(){ polyhedron( faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[0,5,2],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[3,8,5],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[6,11,8],[6,9,11],[0,2,1],[9,10,11]], From b8eda787cae262c1a47d112d88a0b271c4805226 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Thu, 2 Jan 2020 14:59:10 -0600 Subject: [PATCH 06/90] Rewrote and streamlined fillet_2d() and offset_points(). Formatting and cleanup. Tests for changes --- solid/test/test_utils.py | 32 +-- solid/utils.py | 506 +++++++++++++++++---------------------- 2 files changed, 232 insertions(+), 306 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 116582bc..922d6ed1 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -1,13 +1,15 @@ #! /usr/bin/env python import difflib import unittest - -from euclid3 import Point3, Vector3 +import re +from euclid3 import Point3, Vector3, Point2 from solid import scad_render from solid.objects import cube, polygon, sphere, translate from solid.test.ExpandedTestCase import DiffOutput -from solid.utils import BoundingBox, arc, arc_inverted, euc_to_arr, euclidify, extrude_along_path, fillet_2d, is_scad, offset_points, split_body_planar, transform_to_point +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 from solid.utils import FORWARD_VEC, RIGHT_VEC, UP_VEC from solid.utils import back, down, forward, left, right, up @@ -52,6 +54,9 @@ class TestSPUtils(DiffOutput): # 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 test_split_body_planar(self): offset = [10, 10, 10] @@ -78,21 +83,20 @@ def test_split_body_planar(self): def test_fillet_2d_add(self): pts = [[0, 5], [5, 5], [5, 0], [10, 0], [10, 10], [0, 10], ] p = polygon(pts) - newp = fillet_2d(euclidify(pts[0:3], Point3), orig_poly=p, fillet_rad=2, remove_material=False) - expected = '\n\nunion() {\n\tpolygon(paths = [[0, 1, 2, 3, 4, 5]], points = [[0, 5], [5, 5], [5, 0], [10, 0], [10, 10], [0, 10]]);\n\ttranslate(v = [3.0000000000, 3.0000000000, 0.0000000000]) {\n\t\tdifference() {\n\t\t\tintersection() {\n\t\t\t\trotate(a = 358.0000000000) {\n\t\t\t\t\ttranslate(v = [-998, 0, 0]) {\n\t\t\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trotate(a = 452.0000000000) {\n\t\t\t\t\ttranslate(v = [-998, -1000, 0]) {\n\t\t\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcircle(r = 2);\n\t\t}\n\t}\n}' + three_points = [euclidify(pts[0:3], Point2)] + newp = fillet_2d(three_points, orig_poly=p, fillet_rad=2, remove_material=False) + expected = 'union(){polygon(paths=[[0,1,2,3,4,5]],points=[[0,5],[5,5],[5,0],[10,0],[10,10],[0,10]]);translate(v=[3.0000000000,3.0000000000]){difference(){intersection(){rotate(a=359.9000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=450.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' actual = scad_render(newp) - self.assertEqual(expected, actual) + self.assertEqualNoWhitespace(expected, actual) def test_fillet_2d_remove(self): - pts = tri - poly = polygon(euc_to_arr(tri)) - - newp = fillet_2d(tri, orig_poly=poly, fillet_rad=2, remove_material=True) - expected = '\n\ndifference() {\n\tpolygon(paths = [[0, 1, 2]], points = [[0, 0, 0], [10, 0, 0], [0, 10, 0]]);\n\ttranslate(v = [5.1715728753, 2.0000000000, 0.0000000000]) {\n\t\tdifference() {\n\t\t\tintersection() {\n\t\t\t\trotate(a = 268.0000000000) {\n\t\t\t\t\ttranslate(v = [-998, 0, 0]) {\n\t\t\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trotate(a = 407.0000000000) {\n\t\t\t\t\ttranslate(v = [-998, -1000, 0]) {\n\t\t\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcircle(r = 2);\n\t\t}\n\t}\n}' + pts = list((project_to_2D(p) for p in tri)) + poly = polygon(euc_to_arr(pts)) + newp = fillet_2d([pts], orig_poly=poly, fillet_rad=2, remove_material=True) + expected = 'difference(){polygon(paths=[[0,1,2]],points=[[0,0],[10,0],[0,10]]);translate(v=[5.1715728753,2.0000000000]){difference(){intersection(){rotate(a=-90.1000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=45.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' actual = scad_render(newp) - if expected != actual: - print(''.join(difflib.unified_diff(expected, actual))) - self.assertEqual(expected, actual) + + self.assertEqualNoWhitespace(expected, actual) def test_generator_scad(func, args, expected): diff --git a/solid/utils.py b/solid/utils.py index ed16d667..5fd70f43 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -1,29 +1,39 @@ #! /usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import division -import sys from itertools import zip_longest from math import pi, ceil, floor, sqrt, atan2, degrees, radians from solid import union, cube, translate, rotate, square, circle, polyhedron -from solid import difference, intersection, multmatrix +from solid import difference, intersection, multmatrix, cylinder, color from solid import run_euclid_patch from solid import OpenSCADObject, P2, P3, P4, Vec3 , Vec4, Vec34, P3s, P23 from solid import Points, Indexes, ScadSize -from typing import Union, Tuple, Sequence, List, Optional, Callable - - from euclid3 import Point2, Point3, Vector2, Vector3, Line2, Line3 from euclid3 import LineSegment2, LineSegment3, Matrix4 run_euclid_patch() +# ========== +# = TYPING = +# ========== +from typing import Union, Tuple, Sequence, List, Optional, Callable, Dict, cast +Point23 = Union[Point2, Point3] +Vector23 = Union[Vector2, Vector3] +Line23 = Union[Line2, Line3] +LineSegment23 = Union[LineSegment2, LineSegment3] + +Tuple2 = Tuple[float, float] +Tuple3 = Tuple[float, float, float] EucOrTuple = Union[Point3, Vector3, - Tuple[float, float], - Tuple[float, float, float] + Tuple2, + Tuple3 ] +DirectionLR = float # LEFT or RIGHT in 2D + +# ============= +# = CONSTANTS = +# ============= EPSILON = 0.01 RIGHT, TOP, LEFT, BOTTOM = range(4) @@ -138,32 +148,24 @@ def distribute_in_grid(objects:Sequence[OpenSCADObject], # ============== # = Directions = # ============== - - def up(z:float) -> OpenSCADObject: return translate((0, 0, z)) - def down(z: float) -> OpenSCADObject: return translate((0, 0, -z)) - def right(x: float) -> OpenSCADObject: return translate((x, 0, 0)) - def left(x: float) -> OpenSCADObject: return translate((-x, 0, 0)) - def forward(y: float) -> OpenSCADObject: return translate((0, y, 0)) - def back(y: float) -> OpenSCADObject: return translate((0, -y, 0)) - # =========================== # = Box-alignment rotations = # =========================== @@ -171,27 +173,21 @@ def rot_z_to_up(obj:OpenSCADObject) -> OpenSCADObject: # NOTE: Null op return rotate(a=0, v=FORWARD_VEC)(obj) - def rot_z_to_down(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=180, v=FORWARD_VEC)(obj) - def rot_z_to_right(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=90, v=FORWARD_VEC)(obj) - def rot_z_to_left(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=-90, v=FORWARD_VEC)(obj) - def rot_z_to_forward(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=-90, v=RIGHT_VEC)(obj) - def rot_z_to_back(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=90, v=RIGHT_VEC)(obj) - # ================================ # = Box-aligment and translation = # ================================ @@ -219,35 +215,27 @@ def box_align(obj:OpenSCADObject, # = 90-degree Rotations = # ======================= - def rot_z_to_x(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=90, v=FORWARD_VEC)(obj) - def rot_z_to_neg_x(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=-90, v=FORWARD_VEC)(obj) - def rot_z_to_neg_y(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=90, v=RIGHT_VEC)(obj) - def rot_z_to_y(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=-90, v=RIGHT_VEC)(obj) - def rot_x_to_y(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=90, v=UP_VEC)(obj) - def rot_x_to_neg_y(obj:OpenSCADObject) -> OpenSCADObject: return rotate(a=-90, v=UP_VEC)(obj) # ======= # = Arc = # ======= - - def arc(rad:float, start_degrees:float, end_degrees:float, segments:int=None) -> OpenSCADObject: # Note: the circle that this arc is drawn from gets segments, # not the arc itself. That means a quarter-circle arc will @@ -276,7 +264,6 @@ def arc(rad:float, start_degrees:float, end_degrees:float, segments:int=None) -> return ret - def arc_inverted(rad:float, start_degrees:float, end_degrees:float, segments:int=None) -> OpenSCADObject: # Return the segment of an arc *outside* the circle of radius rad, # bounded by two tangents to the circle. This is the shape @@ -336,8 +323,6 @@ def arc_inverted(rad:float, start_degrees:float, end_degrees:float, segments:int # ====================== # = Bounding Box Class = # ====================== - - class BoundingBox(object): # A basic Bounding Box representation to enable some more introspection about # objects. For instance, a BB will let us say "put this new object on top of @@ -349,26 +334,29 @@ class BoundingBox(object): # Basically you can use a BoundingBox to describe the extents of an object # the moment it's created, but once you perform any CSG operation on it, it's # more or less useless. - def __init__(self, size, loc=None): + def __init__(self, size:List[float], loc: List[float]=None): loc = loc if loc else [0, 0, 0] # self.w, self.h, self.d = size # self.x, self.y, self.z = loc self.set_size(size) self.set_position(loc) - def size(self): + def size(self) -> List[float]: return [self.w, self.h, self.d] - def position(self): + def position(self) -> List[float]: return [self.x, self.y, self.z] - def set_position(self, position): + def set_position(self, position: Sequence[float]): self.x, self.y, self.z = position - def set_size(self, size): + def set_size(self, size:Sequence[float]): self.w, self.h, self.d = size - def split_planar(self, cutting_plane_normal=RIGHT_VEC, cut_proportion=0.5, add_wall_thickness=0): + def split_planar(self, + cutting_plane_normal: Vec3=RIGHT_VEC, + cut_proportion: float=0.5, + add_wall_thickness:float=0) -> List['BoundingBox']: cpd = {RIGHT_VEC: 0, LEFT_VEC: 0, FORWARD_VEC: 1, BACK_VEC: 1, UP_VEC: 2, DOWN_VEC: 2} cutting_plane = cpd.get(cutting_plane_normal, 2) @@ -382,7 +370,7 @@ def split_planar(self, cutting_plane_normal=RIGHT_VEC, cut_proportion=0.5, add_w # Now create bounding boxes with the appropriate sizes part_bbs = [] - a_sum = 0 + a_sum = 0.0 for i, part in enumerate([cut_proportion, (1 - cut_proportion)]): part_size = self.size() part_size[cutting_plane] = part_size[cutting_plane] * part @@ -410,32 +398,24 @@ def split_planar(self, cutting_plane_normal=RIGHT_VEC, cut_proportion=0.5, add_w return part_bbs - def cube(self, larger=False): + def cube(self, larger: bool=False) -> OpenSCADObject: c_size = self.size() if not larger else [s + 2 * EPSILON for s in self.size()] c = translate(self.position())( cube(c_size, center=True) ) return c - def min(self, which_dim=None): - min_pt = [p - s / 2 for p, s in zip(self.position(), self.size())] - if which_dim: - return min_pt[which_dim] - else: - return min_pt - - def max(self, which_dim=None): - max_pt = [p + s / 2 for p, s in zip(self.position(), self.size())] - if which_dim: - return max_pt[which_dim] - else: - return max_pt - - # =================== # = Model Splitting = # =================== -def split_body_planar(obj, obj_bb, cutting_plane_normal=UP_VEC, cut_proportion=0.5, dowel_holes=False, dowel_rad=4.5, hole_depth=15, add_wall_thickness=0): +def split_body_planar(obj: OpenSCADObject, + obj_bb: BoundingBox, + cutting_plane_normal: Vec3=UP_VEC, + cut_proportion: float=0.5, + dowel_holes: bool=False, + dowel_rad: float=4.5, + hole_depth: float=15, + add_wall_thickness=0) -> Tuple[OpenSCADObject, BoundingBox, OpenSCADObject, BoundingBox]: # Split obj along the specified plane, returning two pieces and # general bounding boxes for each. # Note that the bounding boxes are NOT accurate to the sections, @@ -486,11 +466,10 @@ def split_body_planar(obj, obj_bb, cutting_plane_normal=UP_VEC, cut_proportion=0 # subtract dowels from each slice slices = [s - dowels for s in slices] - slices_and_bbs = [slices[0], part_bbs[0], slices[1], part_bbs[1]] + slices_and_bbs = (slices[0], part_bbs[0], slices[1], part_bbs[1]) return slices_and_bbs - -def section_cut_xz(body, y_cut_point=0): +def section_cut_xz(body: OpenSCADObject, y_cut_point:float=0) -> OpenSCADObject: big_w = 10000 d = 2 c = forward(d / 2 + y_cut_point)(cube([big_w, d, big_w], center=True)) @@ -518,7 +497,7 @@ def set_bom_headers(*args): global g_bom_headers g_bom_headers += args -def bom_part(description='', per_unit_price=None, currency='US$', *args, **kwargs): +def bom_part(description: str='', per_unit_price:float=None, currency: str='US$', *args, **kwargs) -> Callable: def wrap(f): name = description if description else f.__name__ @@ -539,13 +518,13 @@ def wrapped_f(*wargs, **wkwargs): return wrap -def bill_of_materials(csv=False): +def bill_of_materials(csv:bool=False) -> str: field_names = ["Description", "Count", "Unit Price", "Total Price"] field_names += g_bom_headers rows = [] - all_costs = {} + all_costs: Dict[str, float] = {} for desc, elements in g_parts_dict.items(): count = elements['Count'] currency = elements['currency'] @@ -584,10 +563,10 @@ def bill_of_materials(csv=False): return res -def _currency_str(value, currency="$"): +def _currency_str(value:float, currency: str="$") -> str: return "{currency:>4} {value:.2f}".format(**vars()) -def _table_string(field_names, rows, csv=False): +def _table_string(field_names: Sequence[str], rows:Sequence[Sequence[float]], csv:bool=False) -> str: # Output a justified table string using the prettytable module. # Fall back to Excel-ready tab-separated values if prettytable's not found # or CSV is requested @@ -614,9 +593,7 @@ def _table_string(field_names, rows, csv=False): # ================ # = Bounding Box = # ================ - - -def bounding_box(points): +def bounding_box(points: Sequence[EucOrTuple]) -> Tuple[Tuple3, Tuple3]: all_x = [] all_y = [] all_z = [] @@ -624,12 +601,11 @@ def bounding_box(points): all_x.append(p[0]) all_y.append(p[1]) if len(p) > 2: - all_z.append(p[2]) + all_z.append(p[2]) # type:ignore else: all_z.append(0) - return [[min(all_x), min(all_y), min(all_z)], [max(all_x), max(all_y), max(all_z)]] - + return ((min(all_x), min(all_y), min(all_z)), (max(all_x), max(all_y), max(all_z))) # ======================= # = Hardware dimensions = @@ -638,7 +614,6 @@ def bounding_box(points): 'm3': {'nut_thickness': 2.4, 'nut_inner_diam': 5.4, 'nut_outer_diam': 6.1, 'screw_outer_diam': 3.0, 'cap_diam': 5.5, 'cap_height': 3.0}, 'm4': {'nut_thickness': 3.1, 'nut_inner_diam': 7.0, 'nut_outer_diam': 7.9, 'screw_outer_diam': 4.0, 'cap_diam': 6.9, 'cap_height': 3.9}, 'm5': {'nut_thickness': 4.7, 'nut_inner_diam': 7.9, 'nut_outer_diam': 8.8, 'screw_outer_diam': 5.0, 'cap_diam': 8.7, 'cap_height': 5}, - } bearing_dimensions = { '608': {'inner_d':8, 'outer_d':22, 'thickness':7}, @@ -652,7 +627,7 @@ def bounding_box(points): '633': {'inner_d':3, 'outer_d':13, 'thickness':5}, } -def screw(screw_type='m3', screw_length=16): +def screw(screw_type:str='m3', screw_length:float=16) -> OpenSCADObject: dims = screw_dimensions[screw_type.lower()] shaft_rad = dims['screw_outer_diam'] / 2 cap_rad = dims['cap_diam'] / 2 @@ -666,7 +641,7 @@ def screw(screw_type='m3', screw_length=16): ) return ret -def nut(screw_type='m3'): +def nut(screw_type:str='m3') -> OpenSCADObject: dims = screw_dimensions[screw_type.lower()] outer_rad = dims['nut_outer_diam'] inner_rad = dims['screw_outer_diam'] @@ -677,7 +652,7 @@ def nut(screw_type='m3'): ) return ret -def bearing(bearing_type='624'): +def bearing(bearing_type: str='624') -> OpenSCADObject: dims = bearing_dimensions[bearing_type.lower()] outerR = dims['outer_d']/2 innerR = dims['inner_d']/2 @@ -694,9 +669,9 @@ def bearing(bearing_type='624'): # ================== # = PyEuclid Utils = -# = -------------- = +# ================== def euclidify(an_obj:EucOrTuple, - intended_class=Vector3) -> Union[Point3, Vector3]: + intended_class=Vector3) -> Union[Point23, Vector23]: # If an_obj is an instance of the appropriate PyEuclid class, # return it. Otherwise, try to turn an_obj into the appropriate # class and throw an exception on failure @@ -745,6 +720,22 @@ def euc_to_arr(euc_obj_or_list: EucOrTuple) -> List[float]: # Inverse of euclid result = euc_obj_or_list # type: ignore return result +def project_to_2D(euc_obj:Union[Point23, Vector23]) -> Union[Vector2, Point2]: + """ + Given a Point3/Vector3, return a Point2/Vector2 ignoring the original Z coordinate + """ + result:Union[Vector2, Point2] = None + if isinstance(euc_obj, (Point2, Vector2)): + result = euc_obj + elif isinstance(euc_obj, Point3): + result = Point2(euc_obj.x, euc_obj.y) + elif isinstance(euc_obj, Vector3): + result = Vector2(euc_obj.x, euc_obj.y) + else: + raise ValueError(f"Can't transform object {euc_obj} to a Point2 or Vector2") + + return result + def is_scad(obj:OpenSCADObject) -> bool: return isinstance(obj, OpenSCADObject) @@ -759,7 +750,12 @@ def scad_matrix(euclid_matrix4): # ============== # = Transforms = # ============== -def transform_to_point(body, dest_point, dest_normal, src_point=Point3(0, 0, 0), src_normal=Vector3(0, 1, 0), src_up=Vector3(0, 0, 1)): +def transform_to_point( body: OpenSCADObject, + dest_point: Point3, + dest_normal: Vector3, + src_point: Point3=Point3(0, 0, 0), + src_normal: Vector3=Vector3(0, 1, 0), + src_up: Vector3=Vector3(0, 0, 1)) -> OpenSCADObject: # Transform body to dest_point, looking at dest_normal. # Orientation & offset can be changed by supplying the src arguments @@ -814,13 +810,14 @@ def _orig_euclid_look_at(eye, at, up): res = look_at_matrix * body return res - - # ======================================== # = Vector drawing: 3D arrow from a line = -# = -------------- ======================= -def draw_segment(euc_line=None, endless=False, arrow_rad=7, vec_color=None): - # Draw a tradtional arrow-head vector in 3-space. +# ======================================== +def draw_segment(euc_line: Union[Vector3, Line3]=None, + endless:bool=False, + arrow_rad:float=7, + vec_color: Union[str, Tuple3]=None) -> OpenSCADObject: + # Draw a traditional arrow-head vector in 3-space. vec_arrow_rad = arrow_rad vec_arrow_head_rad = vec_arrow_rad * 1.5 vec_arrow_head_length = vec_arrow_rad * 3 @@ -856,235 +853,167 @@ def draw_segment(euc_line=None, endless=False, arrow_rad=7, vec_color=None): # ========== # = Offset = -# = ------ = -LEFT, RIGHT = radians(90), radians(-90) - -def offset_points(point_arr, offset, inside=True, closed_poly=True): - # Given a set of points, return a set of points offset from - # them. - # To get reasonable results, the points need to be all in a plane. - # (Non-planar point_arr will still return results, but what constitutes - # 'inside' or 'outside' would be different in that situation.) - # - # What direction inside and outside lie in is determined by the first - # three points (first corner). In a convex closed shape, this corresponds - # to inside and outside. If the first three points describe a concave - # portion of a closed shape, inside and outside will be switched. - # - # Basically this means that if you're offsetting a complicated shape, - # you'll likely have to try both directions (inside=True/False) to - # figure out which direction you're offsetting to. - # - # CAD programs generally require an interactive user choice about which - # side is outside and which is inside. Robust behavior with this - # function will require similar checking. - - # Also note that short segments or narrow areas can cause problems - # as well. This method suffices for most planar convex figures where - # segment length is greater than offset, but changing any of those - # assumptions will cause unattractive results. If you want real - # offsets, use SolidWorks. - - # TODO: check for self-intersections in the line connecting the - # offset points, and remove them. - - # Using the first three points in point_arr, figure out which direction - # is inside and what plane to put the points in - point_arr = euclidify(point_arr[:], Point3) - in_dir = _inside_direction(*point_arr[0:3]) - normal = _three_point_normal(*point_arr[0:3]) - direction = in_dir if inside else _other_dir(in_dir) - - # Generate offset points for the correct direction - # for all of point_arr. - segs = [] - offset_pts = [] - point_arr += point_arr[0:2] # Add first two points to the end as well - if closed_poly: - for i in range(len(point_arr) - 1): - a, b = point_arr[i:i + 2] - par_seg = _parallel_seg(a, b, normal=normal, offset=offset, direction=direction) - segs.append(par_seg) - if len(segs) > 1: - int_pt = segs[-2].intersect(segs[-1]) - if int_pt: - offset_pts.append(int_pt) - - # When calculating based on a closed curve, we can't find the - # first offset point until all others have been calculated. - # Now that we've done so, put the last point back to first place - last = offset_pts[-1] - offset_pts.insert(0, last) - del(offset_pts[-1]) +# ========== +# TODO: Make a NamedTuple for LEFT_DIR and RIGHT_DIR +LEFT_DIR, RIGHT_DIR = 1,2 - else: - for i in range(len(point_arr) - 2): - a, b = point_arr[i:i + 2] - par_seg = _parallel_seg(a, b, normal=normal, offset=offset, direction=direction) - segs.append(par_seg) - # In an open poly, first and last points will be parallel - # to the first and last segments, not intersecting other segs - if i == 0: - offset_pts.append(par_seg.p1) - elif i == len(point_arr) - 3: - offset_pts.append(segs[-2].p2) - else: - int_pt = segs[-2].intersect(segs[-1]) - if int_pt: - offset_pts.append(int_pt) +def offset_point(a:Point2, b:Point2, c:Point2, offset:float, direction:DirectionLR=LEFT_DIR) -> Point2: + ab_perp = perpendicular_vector(b-a, direction, length=offset) + bc_perp = perpendicular_vector(c-b, direction, length=offset) + + ab_par = Line2(a + ab_perp, b + ab_perp) + bc_par = Line2(b + bc_perp, c + bc_perp) + result = ab_par.intersect(bc_par) + return result + +def offset_points(points:Sequence[Point2], + offset:float, + internal:bool=True, + closed:bool=False) -> List[Point2]: + """ + Given a set of points, return a set of points offset by `offset`, in the + direction specified by `internal`. + + What is internal or external is defined by by the direction of curvature + between the first and second points; for non-convex shapes, we will return + an incorrect (internal points are all external, or vice versa) if the first + segment pair is concave. This could be mitigated with a point_is_in_polygon() + function, but I haven't written that yet. + """ + # Note that we could just call offset_point() repeatedly, but we'd do + # a lot of repeated calculations that way + src_points = list(points) + if closed: + src_points.append(points[0]) + # src_points = cast(points, List) + [points[0]] if closed else points + + vecs = vectors_between_points(src_points) + direction = direction_of_bend(*src_points[:3]) + if not internal: + direction = opposite_direction(direction) + + perp_vecs = list((perpendicular_vector(v, direction=direction, length=offset) for v in vecs)) + + lines: List[Line2] = [] + for perp, a, b in zip(perp_vecs, src_points[:-1], src_points[1:]): + lines.append(Line2(a+perp, b+perp)) + + intersections = list((a.intersect(b) for a,b in zip(lines[:-1], lines[1:]))) + if closed: - return offset_pts + intersections.append(lines[0].intersect(lines[-1])) + else: + # Include offset points at start and end of shape + intersections = [src_points[0] + perp_vecs[0], *intersections, src_points[-1] + perp_vecs[-1]] + return intersections # ================== # = Offset helpers = # ================== -def _parallel_seg(p, q, offset, normal=Vector3(0, 0, 1), direction=LEFT): - # returns a PyEuclid Line3 parallel to pq, in the plane determined - # by p,normal, to the left or right of pq. - v = q - p - angle = direction - - rot_v = v.rotate_around(axis=normal, theta=angle) - rot_v.set_length(offset) - return Line3(p + rot_v, v) - -def _inside_direction(a, b, c, offset=10): - # determines which direction (LEFT, RIGHT) is 'inside' the triangle - # made by a, b, c. If ab and bc are parallel, return LEFT - x = _three_point_normal(a, b, c) - - # Make two vectors (left & right) for each segment. - l_segs = [_parallel_seg(p, q, normal=x, offset=offset, direction=LEFT) for p, q in ((a, b), (b, c))] - r_segs = [_parallel_seg(p, q, normal=x, offset=offset, direction=RIGHT) for p, q in ((a, b), (b, c))] - - # Find their intersections. - p1 = l_segs[0].intersect(l_segs[1]) - p2 = r_segs[0].intersect(r_segs[1]) - - # The only way I've figured out to determine which direction is - # 'inside' or 'outside' a joint is to calculate both inner and outer - # vectors and then to find the intersection point closest to point a. - # This ought to work but it seems like there ought to be a more direct - # way to figure this out. -ETJ 21 Dec 2012 - - # The point that's closer to point a is the inside point. - if a.distance(p1) <= a.distance(p2): - return LEFT - else: - return RIGHT -def _other_dir(left_or_right:int) -> int: - if left_or_right == LEFT: - return RIGHT - else: - return LEFT +def pairwise_zip(l:Sequence) -> zip: # type:ignore + return zip(l[:-1], l[1:]) + +def cross_2d(a:Vector2, b:Vector2) -> float: + """ + scalar value; tells direction of rotation from a to b; + see direction_of_bend() + # See http://www.allenchou.net/2013/07/cross-product-of-2d-vectors/ + """ + return a.x * b.y - a.y * b.x -def _three_point_normal(a:Point3, b:Point3, c:Point3) -> Vector3: - ab = b - a - bc = c - b +def direction_of_bend(a:Point2, b:Point2, c:Point2) -> DirectionLR: + """ + Return LEFT_DIR if angle abc is a turn to the left, otherwise RIGHT_DIR + Returns RIGHT_DIR if ab and bc are colinear + """ + direction = LEFT_DIR if cross_2d(b-a, c-b) > 0 else RIGHT_DIR + return direction + +def opposite_direction(direction:DirectionLR) -> DirectionLR: + return LEFT_DIR if direction == RIGHT_DIR else RIGHT_DIR - seg_ab = Line3(a, ab) - seg_bc = Line3(b, bc) - x = seg_ab.v.cross(seg_bc.v) - return x +def perpendicular_vector(v:Vector2, direction:DirectionLR=RIGHT_DIR, length:float=None) -> Vector2: + perp_vec = v.cross() # Perpendicular right turn + result = perp_vec if direction == RIGHT_DIR else -perp_vec + if length is not None: + result.set_length(length) + return result + +def vectors_between_points(points: Sequence[Point23]) -> List[Vector23]: + """ + Return a list of the vectors from each point in points to the point that follows + """ + vecs = list((b-a for a,b in pairwise_zip(points))) # type:ignore + return vecs # ============= # = 2D Fillet = # ============= -def _widen_angle_for_fillet(start_degrees:float, end_degrees:float) -> Tuple[float, float]: - # Fix start/end degrees as needed; find a way to make an acute angle - if end_degrees < start_degrees: - end_degrees += 360 - if end_degrees - start_degrees >= 180: - start_degrees, end_degrees = end_degrees, start_degrees - - epsilon_degrees = 2 - return start_degrees - epsilon_degrees, end_degrees + epsilon_degrees +def fillet_2d(three_point_sets: Sequence[Tuple[Point23, Point23, Point23]], + orig_poly: OpenSCADObject, + fillet_rad: float, + remove_material: bool=True) -> OpenSCADObject: + """ + Return a polygon with arcs of radius `fillet_rad` added/removed (according to + `remove_material`) to corners specified in `three_point_sets`. -def fillet_2d(three_point_sets:Sequence[Tuple[Point2, Point2, Point2]], - orig_poly:OpenSCADObject, - fillet_rad:float, - remove_material:bool=True) -> OpenSCADObject: - # NOTE: three_point_sets must be a list of sets of three points - # (i.e., a list of 3-tuples of points), even if only one fillet is being done: - # e.g. [[a, b, c]] - # a, b, and c are three points that form a corner at b. - # Return a negative arc (the area NOT covered by a circle) of radius rad - # in the direction of the more acute angle between - - # Note that if rad is greater than a.distance(b) or c.distance(b), for a - # 90-degree corner, the returned shape will include a jagged edge. - - # TODO: use fillet_rad = min(fillet_rad, a.distance(b), c.distance(b)) - - # If a shape is being filleted in several places, it is FAR faster - # to add/ remove its set of shapes all at once rather than - # to cycle through all the points, since each method call requires - # a relatively complex boolean with the original polygon. - # So... three_point_sets is either a list of three Euclid points that - # determine the corner to be filleted, OR, a list of those lists, in - # which case everything will be removed / added at once. - # NOTE that if material is being added (fillets) or removed (rounds) - # each must be called separately. - - if len(three_point_sets) == 3 and isinstance(three_point_sets[0], (Vector2, Vector3)): - three_point_sets = [three_point_sets] # type: ignore - - arc_objs = [] + e.g. Turn a sharp external corner to a rounded one, or add material + to a sharp interior corner to smooth it out. + """ + arc_objs: List[OpenSCADObject] = [] + # TODO: accept Point3s, and project them all to z==0 for three_points in three_point_sets: + a, b, c = (project_to_2D(p) for p in three_points) + ab = a - b + bc = b - c - assert len(three_points) in (2, 3) - # make two vectors out of the three points passed in - a, b, c = euclidify(three_points, Point3) - - # Find the center of the arc we'll have to make - offset = offset_points([a, b, c], offset=fillet_rad, inside=True) - center_pt = offset[1] - - a2, b2, c2, cp2 = [Point2(p.x, p.y) for p in (a, b, c, center_pt)] - - a2b2 = LineSegment2(a2, b2) - c2b2 = LineSegment2(c2, b2) - - # Find the point on each segment where the arc starts; Point2.connect() - # returns a segment with two points; Take the one that's not the - # center - afs = cp2.connect(a2b2) - cfs = cp2.connect(c2b2) - - afp, cfp = [ - seg.p1 if seg.p1 != cp2 else seg.p2 for seg in (afs, cfs)] + direction = direction_of_bend(a, b, c) - a_degs, c_degs = [ - (degrees(atan2(seg.v.y, seg.v.x))) % 360 for seg in (afs, cfs)] + # center lies at the intersection of two lines parallel to + # ab and bc, respectively, each offset from their respective + # line by fillet_rad + ab_perp = perpendicular_vector(ab, direction, length=fillet_rad) + bc_perp = perpendicular_vector(bc, direction, length=fillet_rad) + center = offset_point(a,b,c, offset=fillet_rad, direction=direction) + # start_pt = center + ab_perp + # end_pt = center + bc_perp - start_degs = a_degs - end_degs = c_degs + start_degrees = degrees(atan2(ab_perp.y, ab_perp.x)) + end_degrees = degrees(atan2(bc_perp.y, bc_perp.x)) - # Widen start_degs and end_degs slightly so they overlap the areas + # Widen start_degrees and end_degrees slightly so they overlap the areas # they're supposed to join/ remove. - start_degs, end_degs = _widen_angle_for_fillet(start_degs, end_degs) + start_degrees, end_degrees = _widen_angle_for_fillet(start_degrees, end_degrees) - arc_obj = translate(center_pt.as_arr())( - arc_inverted( - rad=fillet_rad, start_degrees=start_degs, end_degrees=end_degs) + arc_obj = translate(center.as_arr())( + arc_inverted(rad=fillet_rad, start_degrees=start_degrees, end_degrees=end_degrees) ) - arc_objs.append(arc_obj) if remove_material: - poly = orig_poly - arc_objs # type: ignore + poly = orig_poly - arc_objs else: - poly = orig_poly + arc_objs # type: ignore + poly = orig_poly + arc_objs - return poly + return poly + +def _widen_angle_for_fillet(start_degrees:float, end_degrees:float) -> Tuple[float, float]: + # Fix start/end degrees as needed; find a way to make an acute angle + if end_degrees < start_degrees: + end_degrees += 360 + + if end_degrees - start_degrees >= 180: + start_degrees, end_degrees = end_degrees, start_degrees + + epsilon_degrees = 0.1 + return start_degrees - epsilon_degrees, end_degrees + epsilon_degrees # ========================== # = Extrusion along a path = -# = ---------------------- = -# Possible: twist -def extrude_along_path(shape_pts:Points, +# ========================== +def extrude_along_path( shape_pts:Points, path_pts:Points, scale_factors:Sequence[float]=None) -> OpenSCADObject: # Extrude the convex curve defined by shape_pts along path_pts. @@ -1163,13 +1092,10 @@ def extrude_along_path(shape_pts:Points, return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore - - -# {{{ http://code.activestate.com/recipes/577068/ (r1) - - def frange(*args): - """frange([start, ] end [, step [, mode]]) -> generator + """ + # {{{ http://code.activestate.com/recipes/577068/ (r1) + frange([start, ] end [, step [, mode]]) -> generator A float range generator. If not specified, the default start is 0.0 and the default step is 1.0. @@ -1224,13 +1150,9 @@ def frange(*args): i += 1 x = start + i * step -# end of http://code.activestate.com/recipes/577068/ }}} - # ===================== # = D e b u g g i n g = # ===================== - - def obj_tree_str(sp_obj:OpenSCADObject, vars_to_print:Sequence[str]=None) -> str: # For debugging. This prints a string of all of an object's # children, with whatever attributes are specified in vars_to_print From b6d036fa61834baa746f0e8529914c8e303ccfa7 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Thu, 2 Jan 2020 16:23:15 -0600 Subject: [PATCH 07/90] Added `inverse_thread_direction` argument --- solid/screw_thread.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/solid/screw_thread.py b/solid/screw_thread.py index e2bb9df7..e0f4192a 100755 --- a/solid/screw_thread.py +++ b/solid/screw_thread.py @@ -33,7 +33,8 @@ def thread(outline_pts: Points, segments_per_rot: int = 32, neck_in_degrees: float = 0, neck_out_degrees: float = 0, - rad_2: float=None): + rad_2: float=None, + inverse_thread_direction:bool=False): """ Sweeps outline_pts (an array of points describing a closed polygon in XY) through a spiral. @@ -80,6 +81,13 @@ def thread(outline_pts: Points, threads, (i.e., pitch=tooth_height), I use pitch= tooth_height+EPSILON, since pitch=tooth_height will self-intersect for rotations >=1 """ + # FIXME: For small segments_per_rot where length is not a multiple of + # pitch, the the generated spiral will have irregularities, since we + # don't ensure that each level's segments are in line with those above or + # below. This would require a change in logic to fix. For now, larger values + # of segments_per_rot and length that divides pitch evenly should avoid this issue + # -ETJ 02 January 2020 + rad_2 = rad_2 or inner_rad rotations = length / pitch @@ -148,7 +156,8 @@ def thread(outline_pts: Points, # create new points for p in euc_points: - pt = (p + elev_vec).rotate_around(axis=euc_up, theta=radians(angle)) + theta = radians(angle) * (-1 if inverse_thread_direction else 1) + pt = (p + elev_vec).rotate_around(axis=euc_up, theta=theta) all_points.append(pt.as_arr()) # Add the connectivity information @@ -166,8 +175,13 @@ def thread(outline_pts: Points, all_tris.append([0, i + 2, i + 1]) all_tris.append([last_loop, last_loop + i + 1, last_loop + i + 2]) - # Make the polyhedron - a = polyhedron(points=all_points, faces=all_tris) + # Moving in the opposite direction, we need to reverse the order of + # corners in each face so the OpenSCAD preview renders correctly + if inverse_thread_direction: + all_tris = list([reversed(trio) for trio in all_tris]) + + # Make the polyhedron; convexity info needed for correct OpenSCAD render + a = polyhedron(points=all_points, faces=all_tris, convexity=2) if external: # Intersect with a cylindrical tube to make sure we fit into @@ -178,11 +192,8 @@ def thread(outline_pts: Points, # If the threading is internal, intersect with a central cylinder # to make sure nothing else remains tube = cylinder(r1=inner_rad, r2=rad_2, h=length, segments=segments_per_rot) - # FIXME: For reasons I haven't yet sussed out, the cylinder `tube` doesn't - # line up perfectly with the polyhedron `a`, which creates tiny extra facets - # at joints. These aren't large enough to mess up 3D prints, but they - # do make the shape messier than it needs to be. -ETJ 30 December 2019 - a *= tube + a *= tube + return a def default_thread_section(tooth_height: float, tooth_depth: float): From a9de6a5b6da39acff5bf00987b6824e393bb9105 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Fri, 17 Jan 2020 17:17:13 -0600 Subject: [PATCH 08/90] include `convexity` arg in tests --- solid/test/test_screw_thread.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/solid/test/test_screw_thread.py b/solid/test/test_screw_thread.py index 25a2147a..3860b931 100755 --- a/solid/test/test_screw_thread.py +++ b/solid/test/test_screw_thread.py @@ -30,6 +30,7 @@ def test_thread(self): actual = scad_render(actual_obj) expected = '''intersection(){ polyhedron( + convexity=2, faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[0,5,2],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[3,8,5],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[6,11,8],[6,9,11],[0,2,1],[9,10,11]], points=[[14.9900000000,0.0000000000,-5.0000000000],[19.9900000000,0.0000000000,0.0000000000],[14.9900000000,0.0000000000,5.0000000000],[0.0000000000,20.0000000000,-2.5000000000],[0.0000000000,25.0000000000,2.5000000000],[0.0000000000,20.0000000000,7.5000000000],[-20.0000000000,0.0000000000,0.0000000000],[-25.0000000000,0.0000000000,5.0000000000],[-20.0000000000,0.0000000000,10.0000000000],[-0.0000000000,-14.9900000000,2.5000000000],[-0.0000000000,-19.9900000000,7.5000000000],[-0.0000000000,-14.9900000000,12.5000000000]] ); @@ -52,6 +53,7 @@ def test_thread_internal(self): actual = scad_render(actual_obj) expected = '''intersection() { polyhedron( + convexity=2, faces = [[0, 1, 3], [1, 4, 3], [1, 2, 4], [2, 5, 4], [0, 5, 2], [0, 3, 5], [3, 4, 6], [4, 7, 6], [4, 5, 7], [5, 8, 7], [3, 8, 5], [3, 6, 8], [6, 7, 9], [7, 10, 9], [7, 8, 10], [8, 11, 10], [6, 11, 8], [6, 9, 11], [9, 10, 12], [10, 13, 12], [10, 11, 13], [11, 14, 13], [9, 14, 11], [9, 12, 14], [0, 2, 1], [12, 13, 14]], points = [[25.0100000000, 0.0000000000, 5.0000000000], [20.0100000000, 0.0000000000, 0.0000000000], [25.0100000000, 0.0000000000, -5.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [0.0000000000, 15.0000000000, 5.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [-20.0000000000, 0.0000000000, 15.0000000000], [-15.0000000000, 0.0000000000, 10.0000000000], [-20.0000000000, 0.0000000000, 5.0000000000], [-0.0000000000, -20.0000000000, 20.0000000000], [-0.0000000000, -15.0000000000, 15.0000000000], [-0.0000000000, -20.0000000000, 10.0000000000], [25.0100000000, -0.0000000000, 25.0000000000], [20.0100000000, -0.0000000000, 20.0000000000], [25.0100000000, -0.0000000000, 15.0000000000]] ); @@ -71,7 +73,7 @@ def test_conical_thread_external(self): external=True) actual = scad_render(actual_obj) expected = '''intersection(){ - polyhedron( + polyhedron(convexity=2, faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[0,5,2],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[3,8,5],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[6,11,8],[6,9,11],[0,2,1],[9,10,11]], points=[[5.9450623365,0.0000000000,-1.7556172079],[12.3823254323,0.0000000000,-4.6816458878],[15.3083541122,0.0000000000,1.7556172079],[0.0000000000,21.9850207788,0.7443827921],[0.0000000000,28.4222838746,-2.1816458878],[0.0000000000,31.3483125545,4.2556172079],[-28.6516874455,0.0000000000,3.2443827921],[-35.0889505413,0.0000000000,0.3183541122],[-38.0149792212,0.0000000000,6.7556172079],[-0.0000000000,-25.9450623365,5.7443827921],[-0.0000000000,-32.3823254323,2.8183541122],[-0.0000000000,-35.3083541122,9.2556172079]] ); @@ -95,9 +97,12 @@ def test_conical_thread_internal(self): actual = scad_render(actual_obj) expected = '''intersection(){ polyhedron( + convexity=2, faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[0,5,2],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[3,8,5],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[6,11,8],[6,9,11],[0,2,1],[9,10,11]], - points=[[34.0549376635,0.0000000000,1.7556172079],[27.6176745677,0.0000000000,4.6816458878],[24.6916458878,0.0000000000,-1.7556172079],[0.0000000000,31.3483125545,4.2556172079],[0.0000000000,24.9110494587,7.1816458878],[0.0000000000,21.9850207788,0.7443827921],[-38.0149792212,0.0000000000,6.7556172079],[-31.5777161254,0.0000000000,9.6816458878],[-28.6516874455,0.0000000000,3.2443827921],[-0.0000000000,-54.0549376635,9.2556172079],[-0.0000000000,-47.6176745677,12.1816458878],[-0.0000000000,-44.6916458878,5.7443827921]] - );cylinder($fn=4,h=7.5000000000,r1=20,r2=40);}''' + points=[[34.0549376635,0.0000000000,1.7556172079],[27.6176745677,0.0000000000,4.6816458878],[24.6916458878,0.0000000000,-1.7556172079],[0.0000000000,31.3483125545,4.2556172079],[0.0000000000,24.9110494587,7.1816458878],[0.0000000000,21.9850207788,0.7443827921],[-38.0149792212,0.0000000000,6.7556172079],[-31.5777161254,0.0000000000,9.6816458878],[-28.6516874455,0.0000000000,3.2443827921],[-0.0000000000,-54.0549376635,9.2556172079],[-0.0000000000,-47.6176745677,12.1816458878],[-0.0000000000,-44.6916458878,5.7443827921]] + ); + cylinder($fn=4,h=7.5000000000,r1=20,r2=40); + }''' self.assertEqualNoWhitespace(expected, actual) def test_default_thread_section(self): @@ -118,6 +123,7 @@ def test_neck_in_out_degrees(self): actual = scad_render(actual_obj) expected = '''intersection(){ polyhedron( + convexity=2, faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[0,5,2],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[3,8,5],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[6,11,8],[6,9,11],[0,2,1],[9,10,11]], points=[[14.9900000000,0.0000000000,-5.0000000000],[19.9900000000,0.0000000000,0.0000000000],[14.9900000000,0.0000000000,5.0000000000],[0.0000000000,20.0000000000,-2.5000000000],[0.0000000000,25.0000000000,2.5000000000],[0.0000000000,20.0000000000,7.5000000000],[-20.0000000000,0.0000000000,0.0000000000],[-25.0000000000,0.0000000000,5.0000000000],[-20.0000000000,0.0000000000,10.0000000000],[-0.0000000000,-20.0000000000,2.5000000000],[-0.0000000000,-25.0000000000,7.5000000000],[-0.0000000000,-20.0000000000,12.5000000000]] ); From d13f1c65ac7fe3ea442790366319d88b939076de Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Fri, 17 Jan 2020 17:52:33 -0600 Subject: [PATCH 09/90] Test fix. Added `solid.utils.label()` from dave@nerdfever.com, as described in https://github.com/SolidCode/SolidPython/issues/133 --- solid/test/test_utils.py | 9 +++++++-- solid/utils.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 922d6ed1..69ef8d47 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -12,6 +12,7 @@ from solid.utils import split_body_planar, transform_to_point, project_to_2D from solid.utils import FORWARD_VEC, RIGHT_VEC, UP_VEC from solid.utils import back, down, forward, left, right, up +from solid.utils import label tri = [Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)] scad_test_cases = [ @@ -67,8 +68,7 @@ def test_split_body_planar(self): actual_tuple = split_body_planar(body, body_bb, cutting_plane_normal=split_dir, cut_proportion=0.25) actual.append(actual_tuple) - # Ignore the bounding box object that come back, taking only the SCAD - # objects + # Ignore the bounding box object that come back, taking only the SCAD objects actual = [scad_render(a) for splits in actual for a in splits[::2]] expected = ['\n\nintersection() {\n\ttranslate(v = [10, 10, 10]) {\n\t\tsphere(r = 20);\n\t}\n\ttranslate(v = [-5.0000000000, 10, 10]) {\n\t\tcube(center = true, size = [10.0000000000, 40, 40]);\n\t}\n}', @@ -97,6 +97,11 @@ def test_fillet_2d_remove(self): actual = scad_render(newp) self.assertEqualNoWhitespace(expected, actual) + + def test_label(self): + expected = 'translate(v=[0,5.0000000000,0]){resize(newsize=[15,0,0.5000000000]){union(){translate(v=[0,0.0000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="Hello,",valign="baseline");}}translate(v=[0,-11.5000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="World",valign="baseline");}}}}}' + actual = scad_render(label("Hello,\nWorld")) + self.assertEqualNoWhitespace(expected, actual) def test_generator_scad(func, args, expected): diff --git a/solid/utils.py b/solid/utils.py index 5fd70f43..1ff9e7ce 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -4,6 +4,7 @@ from solid import union, cube, translate, rotate, square, circle, polyhedron from solid import difference, intersection, multmatrix, cylinder, color +from solid import text, linear_extrude, resize from solid import run_euclid_patch from solid import OpenSCADObject, P2, P3, P4, Vec3 , Vec4, Vec34, P3s, P23 @@ -667,6 +668,39 @@ def bearing(bearing_type: str='624') -> OpenSCADObject: ) return bearing +# ========= +# = LABEL = +# ========= +def label(a_str:str, width:float=15, halign:str="left", valign:str="baseline", + size:int=10, depth:float=0.5, lineSpacing:float=1.15, + font:str="MgOpen Modata:style=Bold", segments:int=40, spacing:int=1) -> OpenSCADObject: + """Renders a multi-line string into a single 3D object. + + __author__ = 'NerdFever.com' + __copyright__ = 'Copyright 2018-2019 NerdFever.com' + __version__ = '' + __email__ = 'dave@nerdfever.com' + __status__ = 'Development' + __license__ = Copyright 2018-2019 NerdFever.com + """ + + lines = a_str.splitlines() + + texts = [] + + for idx, l in enumerate(lines): + t = text(text=l, halign=halign, valign=valign, font=font, spacing=spacing).add_param('$fn', segments) + t = linear_extrude(height=1)(t) + t = translate([0, -size * idx * lineSpacing, 0])(t) + + texts.append(t) + + result = union()(texts) + result = resize([width, 0, depth])(result) + result = translate([0, (len(lines)-1)*size / 2, 0])(result) + + return result + # ================== # = PyEuclid Utils = # ================== @@ -934,7 +968,7 @@ def opposite_direction(direction:DirectionLR) -> DirectionLR: return LEFT_DIR if direction == RIGHT_DIR else RIGHT_DIR def perpendicular_vector(v:Vector2, direction:DirectionLR=RIGHT_DIR, length:float=None) -> Vector2: - perp_vec = v.cross() # Perpendicular right turn + perp_vec = Vector2(v.y, -v.x) result = perp_vec if direction == RIGHT_DIR else -perp_vec if length is not None: result.set_length(length) From 090e326db9ad4d4e6d1a6ef4c3caa0028ed3319b Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 19 Jan 2020 15:32:59 -0600 Subject: [PATCH 10/90] Reverted run_all_tests.sh to previous explicit test running. Unittest's built-in discovery doesn't run the dynamic testcase generation these test files contain --- solid/test/run_all_tests.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/solid/test/run_all_tests.sh b/solid/test/run_all_tests.sh index 3732b294..e99fc6ab 100755 --- a/solid/test/run_all_tests.sh +++ b/solid/test/run_all_tests.sh @@ -4,8 +4,15 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" cd $DIR -# Let unittest discover all the tests -python -m unittest discover . +# Run all tests. Note that unittest's built-in discovery doesn't run the dynamic +# testcase generation they contain +for i in test_*.py; +do + echo $i; + python $i; + echo +done + # revert to original dir cd - \ No newline at end of file From a729e159c7533e97f1576f1fe43da56b33e76573 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 19 Jan 2020 15:33:56 -0600 Subject: [PATCH 11/90] chmod +x --- solid/test/test_splines.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 solid/test/test_splines.py diff --git a/solid/test/test_splines.py b/solid/test/test_splines.py old mode 100644 new mode 100755 From 0624cc3d402830dc0e8a416336ce8a2268d76021 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 19 Jan 2020 15:35:51 -0600 Subject: [PATCH 12/90] Fixes to offset_points() & tests. The concept of 'open' or 'closed' didn't make much sense when returning a list of points; removed the 'closed' arg --- solid/test/test_utils.py | 35 +++++++++++++++++++++-------------- solid/utils.py | 25 +++++++++++++------------ 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 69ef8d47..cf6116a2 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -15,15 +15,17 @@ from solid.utils import label tri = [Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)] + scad_test_cases = [ - (up, [2], '\n\ntranslate(v = [0, 0, 2]);'), - (down, [2], '\n\ntranslate(v = [0, 0, -2]);'), - (left, [2], '\n\ntranslate(v = [-2, 0, 0]);'), - (right, [2], '\n\ntranslate(v = [2, 0, 0]);'), - (forward, [2], '\n\ntranslate(v = [0, 2, 0]);'), - (back, [2], '\n\ntranslate(v = [0, -2, 0]);'), - (arc, [10, 0, 90, 24], '\n\ndifference() {\n\tcircle($fn = 24, r = 10);\n\trotate(a = 0) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n\trotate(a = -90) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n}'), - (arc_inverted, [10, 0, 90, 24], '\n\ndifference() {\n\tintersection() {\n\t\trotate(a = 0) {\n\t\t\ttranslate(v = [-990, 0, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t\trotate(a = 90) {\n\t\t\ttranslate(v = [-990, -1000, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t}\n\tcircle($fn = 24, r = 10);\n}'), + # Test name, function, args, expected value + ('up', up, [2], '\n\ntranslate(v = [0, 0, 2]);'), + ('down', down, [2], '\n\ntranslate(v = [0, 0, -2]);'), + ('left', left, [2], '\n\ntranslate(v = [-2, 0, 0]);'), + ('right', right, [2], '\n\ntranslate(v = [2, 0, 0]);'), + ('forward', forward, [2], '\n\ntranslate(v = [0, 2, 0]);'), + ('back', back, [2], '\n\ntranslate(v = [0, -2, 0]);'), + ('arc', arc, [10, 0, 90, 24], '\n\ndifference() {\n\tcircle($fn = 24, r = 10);\n\trotate(a = 0) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n\trotate(a = -90) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n}'), + ('arc_inverted', arc_inverted, [10, 0, 90, 24], '\n\ndifference() {\n\tintersection() {\n\t\trotate(a = 0) {\n\t\t\ttranslate(v = [-990, 0, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t\trotate(a = 90) {\n\t\t\ttranslate(v = [-990, -1000, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t}\n\tcircle($fn = 24, r = 10);\n}'), ('transform_to_point_scad', transform_to_point, [cube(2), [2, 2, 2], [3, 3, 1]], '\n\nmultmatrix(m = [[0.7071067812, -0.1622214211, -0.6882472016, 2], [-0.7071067812, -0.1622214211, -0.6882472016, 2], [0.0000000000, 0.9733285268, -0.2294157339, 2], [0, 0, 0, 1.0000000000]]) {\n\tcube(size = 2);\n}'), ('extrude_along_path', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000]]);'), ('extrude_along_path_vertical', extrude_along_path, [tri, [[0, 0, 0], [0, 0, 20]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [-10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 10.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [-10.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, 10.0000000000, 20.0000000000]]);'), @@ -31,24 +33,24 @@ ] other_test_cases = [ - (euclidify, [[0, 0, 0]], 'Vector3(0.00, 0.00, 0.00)'), + # Test name, function, args, expected value + ('euclidify', euclidify, [[0, 0, 0]], 'Vector3(0.00, 0.00, 0.00)'), ('euclidify_recursive', euclidify, [[[0, 0, 0], [1, 0, 0]]], '[Vector3(0.00, 0.00, 0.00), Vector3(1.00, 0.00, 0.00)]'), ('euclidify_Vector', euclidify, [Vector3(0, 0, 0)], 'Vector3(0.00, 0.00, 0.00)'), ('euclidify_recursive_Vector', euclidify, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[Vector3(0.00, 0.00, 0.00), Vector3(0.00, 0.00, 1.00)]'), - (euc_to_arr, [Vector3(0, 0, 0)], '[0, 0, 0]'), + ('euc_to_arr', euc_to_arr, [Vector3(0, 0, 0)], '[0, 0, 0]'), ('euc_to_arr_recursive', euc_to_arr, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[[0, 0, 0], [0, 0, 1]]'), ('euc_to_arr_arr', euc_to_arr, [[0, 0, 0]], '[0, 0, 0]'), ('euc_to_arr_arr_recursive', euc_to_arr, [[[0, 0, 0], [1, 0, 0]]], '[[0, 0, 0], [1, 0, 0]]'), - (is_scad, [cube(2)], 'True'), + ('is_scad', is_scad, [cube(2)], 'True'), ('is_scad_false', is_scad, [2], 'False'), ('transform_to_point_single_arr', transform_to_point, [[1, 0, 0], [2, 2, 2], [3, 3, 1]], 'Point3(2.71, 1.29, 2.00)'), ('transform_to_point_single_pt3', transform_to_point, [Point3(1, 0, 0), [2, 2, 2], [3, 3, 1]], 'Point3(2.71, 1.29, 2.00)'), ('transform_to_point_arr_arr', transform_to_point, [[[1, 0, 0], [0, 1, 0], [0, 0, 1]], [2, 2, 2], [3, 3, 1]], '[Point3(2.71, 1.29, 2.00), Point3(1.84, 1.84, 2.97), Point3(1.31, 1.31, 1.77)]'), ('transform_to_point_pt3_arr', transform_to_point, [[Point3(1, 0, 0), Point3(0, 1, 0), Point3(0, 0, 1)], [2, 2, 2], [3, 3, 1]], '[Point3(2.71, 1.29, 2.00), Point3(1.84, 1.84, 2.97), Point3(1.31, 1.31, 1.77)]'), ('transform_to_point_redundant', transform_to_point, [[Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)], [2, 2, 2], Vector3(0, 0, 1), Point3(0, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, 1)], '[Point3(2.00, 2.00, 2.00), Point3(-8.00, 2.00, 2.00), Point3(2.00, 12.00, 2.00)]'), - ('offset_points_inside', offset_points, [tri, 2, True], '[Point3(2.00, 2.00, 0.00), Point3(5.17, 2.00, 0.00), Point3(2.00, 5.17, 0.00)]'), - ('offset_points_outside', offset_points, [tri, 2, False], '[Point3(-2.00, -2.00, 0.00), Point3(14.83, -2.00, 0.00), Point3(-2.00, 14.83, 0.00)]'), - ('offset_points_open_poly', offset_points, [tri, 2, False, False], '[Point3(0.00, -2.00, 0.00), Point3(14.83, -2.00, 0.00), Point3(1.41, 11.41, 0.00)]'), + ('offset_points_inside', offset_points, [tri, 2, True], '[Point2(2.00, 2.00), Point2(5.17, 2.00), Point2(2.00, 5.17)]'), + ('offset_points_outside', offset_points, [tri, 2, False], '[Point2(-2.00, -2.00), Point2(14.83, -2.00), Point2(-2.00, 14.83)]'), ] @@ -98,6 +100,11 @@ def test_fillet_2d_remove(self): self.assertEqualNoWhitespace(expected, actual) + # def test_offset_points_inside(self): + # expected = '' + # actual = scad_render(offset_points(tri2d, offset=2, internal=True)) + # self.assertEqualNoWhitespace(expected, actual) + def test_label(self): expected = 'translate(v=[0,5.0000000000,0]){resize(newsize=[15,0,0.5000000000]){union(){translate(v=[0,0.0000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="Hello,",valign="baseline");}}translate(v=[0,-11.5000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="World",valign="baseline");}}}}}' actual = scad_render(label("Hello,\nWorld")) diff --git a/solid/utils.py b/solid/utils.py index 1ff9e7ce..22356cb6 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -900,14 +900,21 @@ def offset_point(a:Point2, b:Point2, c:Point2, offset:float, direction:Direction result = ab_par.intersect(bc_par) return result -def offset_points(points:Sequence[Point2], +def offset_points(points:Sequence[Point23], offset:float, - internal:bool=True, - closed:bool=False) -> List[Point2]: + internal:bool=True) -> List[Point2]: """ Given a set of points, return a set of points offset by `offset`, in the direction specified by `internal`. + NOTE: OpenSCAD has the native `offset()` function that generates offset + polygons nicely as well as doing fillets & rounds. If you just need a shape, + prefer using the native `offset()`. If you need the actual points for some + purpose, use this function. + See: https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Transformations#offset + + # NOTE: We accept Point2s or Point3s, but ignore all Z values and return Point2s + What is internal or external is defined by by the direction of curvature between the first and second points; for non-convex shapes, we will return an incorrect (internal points are all external, or vice versa) if the first @@ -916,10 +923,7 @@ def offset_points(points:Sequence[Point2], """ # Note that we could just call offset_point() repeatedly, but we'd do # a lot of repeated calculations that way - src_points = list(points) - if closed: - src_points.append(points[0]) - # src_points = cast(points, List) + [points[0]] if closed else points + src_points = list((Point2(p.x, p.y) for p in (*points, points[0]))) vecs = vectors_between_points(src_points) direction = direction_of_bend(*src_points[:3]) @@ -933,12 +937,9 @@ def offset_points(points:Sequence[Point2], lines.append(Line2(a+perp, b+perp)) intersections = list((a.intersect(b) for a,b in zip(lines[:-1], lines[1:]))) - if closed: + # First point is determined by intersection of first and last lines + intersections.insert(0, lines[0].intersect(lines[-1])) - intersections.append(lines[0].intersect(lines[-1])) - else: - # Include offset points at start and end of shape - intersections = [src_points[0] + perp_vecs[0], *intersections, src_points[-1] + perp_vecs[-1]] return intersections # ================== From a66fb3062a1499cb4d9f0e9e078ea75894f5fe03 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 18 Mar 2020 12:56:24 -0500 Subject: [PATCH 13/90] =?UTF-8?q?--=20Added=20`segments`=20argument=20to?= =?UTF-8?q?=20`offset()`,=20since=20it=20can=20create=20curves.=20--=20Add?= =?UTF-8?q?ed=20testing=20for=20offset=20change=20and=20fixed=20a=20bug=20?= =?UTF-8?q?in=20test=20generation=20that=20was=20preventing=20multiple=20t?= =?UTF-8?q?ests=20for=20the=20same=20class=20from=20running=E2=80=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- solid/objects.py | 8 +++- solid/test/test_solidpython.py | 73 +++++++++++++++++----------------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index a242529f..b6ca0f1c 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -402,15 +402,21 @@ class offset(OpenSCADObject): should be chamfered (cut off with a straight line) or not (extended to their intersection). :type chamfer: bool + + :param segments: Resolution of any radial curves + :type segments: int """ - def __init__(self, r: float = None, delta: float = None, chamfer: bool = False) -> None: + def __init__(self, r: float = None, delta: float = None, chamfer: bool = False, + segments: int=None) -> None: if r: kwargs = {'r': r} elif delta: kwargs = {'delta': delta, 'chamfer': chamfer} else: raise ValueError("offset(): Must supply r or delta") + if segments: + kwargs['segments'] = segments super().__init__('offset', kwargs) diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index dce2513a..02a64ad1 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -16,40 +16,41 @@ from solid.test.ExpandedTestCase import DiffOutput scad_test_case_templates = [ - {'name': 'polygon', 'kwargs': {'paths': [[0, 1, 2]]}, 'expected': '\n\npolygon(paths = [[0, 1, 2]], points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]]}, }, - {'name': 'circle', 'kwargs': {'segments': 12, 'r': 1}, 'expected': '\n\ncircle($fn = 12, r = 1);', 'args': {}, }, - {'name': 'circle', 'kwargs': {'segments': 12, 'd': 1}, 'expected': '\n\ncircle($fn = 12, d = 1);', 'args': {}, }, - {'name': 'square', 'kwargs': {'center': False, 'size': 1}, 'expected': '\n\nsquare(center = false, size = 1);', 'args': {}, }, - {'name': 'sphere', 'kwargs': {'segments': 12, 'r': 1}, 'expected': '\n\nsphere($fn = 12, r = 1);', 'args': {}, }, - {'name': 'sphere', 'kwargs': {'segments': 12, 'd': 1}, 'expected': '\n\nsphere($fn = 12, d = 1);', 'args': {}, }, - {'name': 'cube', 'kwargs': {'center': False, 'size': 1}, 'expected': '\n\ncube(center = false, size = 1);', 'args': {}, }, - {'name': 'cylinder', 'kwargs': {'r1': None, 'r2': None, 'h': 1, 'segments': 12, 'r': 1, 'center': False}, 'expected': '\n\ncylinder($fn = 12, center = false, h = 1, r = 1);', 'args': {}, }, - {'name': 'cylinder', 'kwargs': {'d1': 4, 'd2': 2, 'h': 1, 'segments': 12, 'center': False}, 'expected': '\n\ncylinder($fn = 12, center = false, d1 = 4, d2 = 2, h = 1);', 'args': {}, }, - {'name': 'polyhedron', 'kwargs': {'convexity': None}, 'expected': '\n\npolyhedron(faces = [[0, 1, 2]], points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]], 'faces': [[0, 1, 2]]}, }, - {'name': 'union', 'kwargs': {}, 'expected': '\n\nunion();', 'args': {}, }, - {'name': 'intersection', 'kwargs': {}, 'expected': '\n\nintersection();', 'args': {}, }, - {'name': 'difference', 'kwargs': {}, 'expected': '\n\ndifference();', 'args': {}, }, - {'name': 'translate', 'kwargs': {'v': [1, 0, 0]}, 'expected': '\n\ntranslate(v = [1, 0, 0]);', 'args': {}, }, - {'name': 'scale', 'kwargs': {'v': 0.5}, 'expected': '\n\nscale(v = 0.5000000000);', 'args': {}, }, - {'name': 'rotate', 'kwargs': {'a': 45, 'v': [0, 0, 1]}, 'expected': '\n\nrotate(a = 45, v = [0, 0, 1]);', 'args': {}, }, - {'name': 'mirror', 'kwargs': {}, 'expected': '\n\nmirror(v = [0, 0, 1]);', 'args': {'v': [0, 0, 1]}, }, - {'name': 'resize', 'kwargs': {'newsize': [5, 5, 5], 'auto': [True, True, False]}, 'expected': '\n\nresize(auto = [true, true, false], newsize = [5, 5, 5]);', 'args': {}, }, - {'name': 'multmatrix', 'kwargs': {}, 'expected': '\n\nmultmatrix(m = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]);', 'args': {'m': [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]}, }, - {'name': 'color', 'kwargs': {}, 'expected': '\n\ncolor(c = [1, 0, 0]);', 'args': {'c': [1, 0, 0]}, }, - {'name': 'minkowski', 'kwargs': {}, 'expected': '\n\nminkowski();', 'args': {}, }, - {'name': 'offset', 'kwargs': {'r': 1}, 'expected': '\n\noffset(r = 1);', 'args': {}, }, - {'name': 'offset', 'kwargs': {'delta': 1}, 'expected': '\n\noffset(chamfer = false, delta = 1);', 'args': {}, }, - {'name': 'hull', 'kwargs': {}, 'expected': '\n\nhull();', 'args': {}, }, - {'name': 'render', 'kwargs': {'convexity': None}, 'expected': '\n\nrender();', 'args': {}, }, - {'name': 'projection', 'kwargs': {'cut': None}, 'expected': '\n\nprojection();', 'args': {}, }, - {'name': 'surface', 'kwargs': {'center': False, 'convexity': None}, 'expected': '\n\nsurface(center = false, file = "/Path/to/dummy.dxf");', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, - {'name': 'import_stl', 'kwargs': {'layer': None, 'origin': (0, 0)}, 'expected': '\n\nimport(file = "/Path/to/dummy.stl", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.stl'"}, }, - {'name': 'import_dxf', 'kwargs': {'layer': None, 'origin': (0, 0)}, 'expected': '\n\nimport(file = "/Path/to/dummy.dxf", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, - {'name': 'import_', 'kwargs': {'layer': None, 'origin': (0, 0)}, 'expected': '\n\nimport(file = "/Path/to/dummy.dxf", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, - {'name': 'import_', 'kwargs': {'layer': None, 'origin': (0, 0), 'convexity': 2}, 'expected': '\n\nimport(convexity = 2, file = "/Path/to/dummy.dxf", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, - {'name': 'linear_extrude', 'kwargs': {'twist': None, 'slices': None, 'center': False, 'convexity': None, 'height': 1, 'scale': 0.9}, 'expected': '\n\nlinear_extrude(center = false, height = 1, scale = 0.9000000000);', 'args': {}, }, - {'name': 'rotate_extrude', 'kwargs': {'angle': 90, 'segments': 4, 'convexity': None}, 'expected': '\n\nrotate_extrude($fn = 4, angle = 90);', 'args': {}, }, - {'name': 'intersection_for', 'kwargs': {}, 'expected': '\n\nintersection_for(n = [0, 1, 2]);', 'args': {'n': [0, 1, 2]}, }, + {'name': 'polygon', 'class': 'polygon' , 'kwargs': {'paths': [[0, 1, 2]]}, 'expected': '\n\npolygon(paths = [[0, 1, 2]], points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]]}, }, + {'name': 'circle', 'class': 'circle' , 'kwargs': {'segments': 12, 'r': 1}, 'expected': '\n\ncircle($fn = 12, r = 1);', 'args': {}, }, + {'name': 'circle_diam', 'class': 'circle' , 'kwargs': {'segments': 12, 'd': 1}, 'expected': '\n\ncircle($fn = 12, d = 1);', 'args': {}, }, + {'name': 'square', 'class': 'square' , 'kwargs': {'center': False, 'size': 1}, 'expected': '\n\nsquare(center = false, size = 1);', 'args': {}, }, + {'name': 'sphere', 'class': 'sphere' , 'kwargs': {'segments': 12, 'r': 1}, 'expected': '\n\nsphere($fn = 12, r = 1);', 'args': {}, }, + {'name': 'sphere_diam', 'class': 'sphere' , 'kwargs': {'segments': 12, 'd': 1}, 'expected': '\n\nsphere($fn = 12, d = 1);', 'args': {}, }, + {'name': 'cube', 'class': 'cube' , 'kwargs': {'center': False, 'size': 1}, 'expected': '\n\ncube(center = false, size = 1);', 'args': {}, }, + {'name': 'cylinder', 'class': 'cylinder' , 'kwargs': {'r1': None, 'r2': None, 'h': 1, 'segments': 12, 'r': 1, 'center': False}, 'expected': '\n\ncylinder($fn = 12, center = false, h = 1, r = 1);', 'args': {}, }, + {'name': 'cylinder_d1d2', 'class': 'cylinder' , 'kwargs': {'d1': 4, 'd2': 2, 'h': 1, 'segments': 12, 'center': False}, 'expected': '\n\ncylinder($fn = 12, center = false, d1 = 4, d2 = 2, h = 1);', 'args': {}, }, + {'name': 'polyhedron', 'class': 'polyhedron' , 'kwargs': {'convexity': None}, 'expected': '\n\npolyhedron(faces = [[0, 1, 2]], points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]], 'faces': [[0, 1, 2]]}, }, + {'name': 'union', 'class': 'union' , 'kwargs': {}, 'expected': '\n\nunion();', 'args': {}, }, + {'name': 'intersection', 'class': 'intersection' , 'kwargs': {}, 'expected': '\n\nintersection();', 'args': {}, }, + {'name': 'difference', 'class': 'difference' , 'kwargs': {}, 'expected': '\n\ndifference();', 'args': {}, }, + {'name': 'translate', 'class': 'translate' , 'kwargs': {'v': [1, 0, 0]}, 'expected': '\n\ntranslate(v = [1, 0, 0]);', 'args': {}, }, + {'name': 'scale', 'class': 'scale' , 'kwargs': {'v': 0.5}, 'expected': '\n\nscale(v = 0.5000000000);', 'args': {}, }, + {'name': 'rotate', 'class': 'rotate' , 'kwargs': {'a': 45, 'v': [0, 0, 1]}, 'expected': '\n\nrotate(a = 45, v = [0, 0, 1]);', 'args': {}, }, + {'name': 'mirror', 'class': 'mirror' , 'kwargs': {}, 'expected': '\n\nmirror(v = [0, 0, 1]);', 'args': {'v': [0, 0, 1]}, }, + {'name': 'resize', 'class': 'resize' , 'kwargs': {'newsize': [5, 5, 5], 'auto': [True, True, False]}, 'expected': '\n\nresize(auto = [true, true, false], newsize = [5, 5, 5]);', 'args': {}, }, + {'name': 'multmatrix', 'class': 'multmatrix' , 'kwargs': {}, 'expected': '\n\nmultmatrix(m = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]);', 'args': {'m': [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]}, }, + {'name': 'color', 'class': 'color' , 'kwargs': {}, 'expected': '\n\ncolor(c = [1, 0, 0]);', 'args': {'c': [1, 0, 0]}, }, + {'name': 'minkowski', 'class': 'minkowski' , 'kwargs': {}, 'expected': '\n\nminkowski();', 'args': {}, }, + {'name': 'offset', 'class': 'offset' , 'kwargs': {'r': 1}, 'expected': '\n\noffset(r = 1);', 'args': {}, }, + {'name': 'offset_segments', 'class': 'offset' , 'kwargs': {'r': 1, 'segments': 12}, 'expected': '\n\noffset($fn = 12, r = 1);', 'args': {}, }, + {'name': 'offset_chamfer', 'class': 'offset' , 'kwargs': {'delta': 1}, 'expected': '\n\noffset(chamfer = false, delta = 1);', 'args': {}, }, + {'name': 'hull', 'class': 'hull' , 'kwargs': {}, 'expected': '\n\nhull();', 'args': {}, }, + {'name': 'render', 'class': 'render' , 'kwargs': {'convexity': None}, 'expected': '\n\nrender();', 'args': {}, }, + {'name': 'projection', 'class': 'projection' , 'kwargs': {'cut': None}, 'expected': '\n\nprojection();', 'args': {}, }, + {'name': 'surface', 'class': 'surface' , 'kwargs': {'center': False, 'convexity': None}, 'expected': '\n\nsurface(center = false, file = "/Path/to/dummy.dxf");', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, + {'name': 'import_stl', 'class': 'import_stl' , 'kwargs': {'layer': None, 'origin': (0, 0)}, 'expected': '\n\nimport(file = "/Path/to/dummy.stl", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.stl'"}, }, + {'name': 'import_dxf', 'class': 'import_dxf' , 'kwargs': {'layer': None, 'origin': (0, 0)}, 'expected': '\n\nimport(file = "/Path/to/dummy.dxf", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, + {'name': 'import_', 'class': 'import_' , 'kwargs': {'layer': None, 'origin': (0, 0)}, 'expected': '\n\nimport(file = "/Path/to/dummy.dxf", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, + {'name': 'import__convexity', 'class': 'import_' , 'kwargs': {'layer': None, 'origin': (0, 0), 'convexity': 2}, 'expected': '\n\nimport(convexity = 2, file = "/Path/to/dummy.dxf", origin = [0, 0]);', 'args': {'file': "'/Path/to/dummy.dxf'"}, }, + {'name': 'linear_extrude', 'class': 'linear_extrude' , 'kwargs': {'twist': None, 'slices': None, 'center': False, 'convexity': None, 'height': 1, 'scale': 0.9}, 'expected': '\n\nlinear_extrude(center = false, height = 1, scale = 0.9000000000);', 'args': {}, }, + {'name': 'rotate_extrude', 'class': 'rotate_extrude' , 'kwargs': {'angle': 90, 'segments': 4, 'convexity': None}, 'expected': '\n\nrotate_extrude($fn = 4, angle = 90);', 'args': {}, }, + {'name': 'intersection_for', 'class': 'intersection_for' , 'kwargs': {}, 'expected': '\n\nintersection_for(n = [0, 1, 2]);', 'args': {'n': [0, 1, 2]}, }, ] @@ -412,10 +413,10 @@ def __iter__(self): def single_test(test_dict): - name, args, kwargs, expected = test_dict['name'], test_dict['args'], test_dict['kwargs'], test_dict['expected'] + name, cls, args, kwargs, expected = test_dict['name'], test_dict['class'], test_dict['args'], test_dict['kwargs'], test_dict['expected'] def test(self): - call_str = name + "(" + call_str = cls + "(" for k, v in args.items(): call_str += f"{k}={v}, " for k, v in kwargs.items(): From 6b0cc72cf5cce32e801d41aa5f608e0c610b33da Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 18 Mar 2020 12:58:02 -0500 Subject: [PATCH 14/90] Version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3cba801c..feba40de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "0.4.6" +version = "0.4.7" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From 9bd8df5c640392ad8cd44ead94330876195b3ab6 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 15 Apr 2020 13:46:35 -0500 Subject: [PATCH 15/90] Updated to v0.4.8 Resolves #139; Spline functions now accept sequences of sequences of floats (e.g. [(1,2), (3.2, 4)] instead of sequences of Point23s --- pyproject.toml | 2 +- solid/splines.py | 32 ++++++++++++++++++++------------ solid/test/test_splines.py | 23 +++++++++++++++++++---- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index feba40de..a31ac912 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "0.4.7" +version = "0.4.8" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" diff --git a/solid/splines.py b/solid/splines.py index e0711425..6fccd576 100644 --- a/solid/splines.py +++ b/solid/splines.py @@ -2,14 +2,15 @@ from math import pow from solid import circle, cylinder, polygon, color, OpenSCADObject, translate, linear_extrude -from solid.utils import bounding_box, right, Red, Tuple3 +from solid.utils import bounding_box, right, Red, Tuple3, euclidify from euclid3 import Vector2, Vector3, Point2, Point3 from typing import Sequence, Tuple, Union, List, cast Point23 = Union[Point2, Point3] +Point23List = Union[Point23, Tuple[float, float], Tuple[float, float, float]] Vec23 = Union[Vector2, Vector3] -FourPoints = Tuple[Point23, Point23, Point23, Point23] +FourPoints = Tuple[Point23List, Point23List, Point23List, Point23List] SEGMENTS = 48 DEFAULT_SUBDIVISIONS = 10 @@ -18,7 +19,7 @@ # ======================= # = CATMULL-ROM SPLINES = # ======================= -def catmull_rom_polygon(points: Sequence[Point23], +def catmull_rom_polygon(points: Sequence[Point23List], subdivisions: int = DEFAULT_SUBDIVISIONS, extrude_height: float = DEFAULT_EXTRUDE_HEIGHT, show_controls: bool =False, @@ -43,7 +44,7 @@ def catmull_rom_polygon(points: Sequence[Point23], shape += control_points(points, extrude_height, center) return shape -def catmull_rom_points( points: Sequence[Point23], +def catmull_rom_points( points: Sequence[Point23List], subdivisions:int = 10, close_loop: bool=False, start_tangent: Vec23 = None, @@ -62,15 +63,17 @@ def catmull_rom_points( points: Sequence[Point23], """ catmull_points: List[Point23] = [] cat_points: List[Point23] = [] - points_list = cast(List[Point23], points) + # points_list = cast(List[Point23], points) + + points_list = list([euclidify(p, Point2) for p in points]) if close_loop: - cat_points = [points[-1]] + points_list + [points[0]] + cat_points = [points_list[-1]] + points_list + [points_list[0]] else: # Use supplied tangents or just continue the ends of the supplied points - start_tangent = start_tangent or (points[1] - points[0]) - end_tangent = end_tangent or (points[-2] - points[-1]) - cat_points = [points[0]+ start_tangent] + points_list + [points[-1] + end_tangent] + start_tangent = start_tangent or (points_list[1] - points_list[0]) + end_tangent = end_tangent or (points_list[-2] - points_list[-1]) + cat_points = [points_list[0]+ start_tangent] + points_list + [points_list[-1] + end_tangent] last_point_range = len(cat_points) - 2 if close_loop else len(cat_points) - 3 @@ -104,7 +107,7 @@ def _catmull_rom_segment(controls: FourPoints, if include_last: num_points += 1 - p0, p1, p2, p3 = controls + p0, p1, p2, p3 = [euclidify(p, Point2) for p in controls] a = 2 * p1 b = p2 - p0 c = 2* p0 - 5*p1 + 4*p2 - p3 @@ -113,7 +116,7 @@ def _catmull_rom_segment(controls: FourPoints, for i in range(num_points): t = i/subdivisions pos = 0.5 * (a + (b * t) + (c * t * t) + (d * t * t * t)) - positions.append(pos) + positions.append(Point2(*pos)) return positions # ================== @@ -162,7 +165,12 @@ def bezier_points(controls: FourPoints, points.append(_point_along_bez4(*controls, u)) return points -def _point_along_bez4(p0: Point23, p1: Point23, p2: Point23, p3: Point23, u:float) -> Point2: +def _point_along_bez4(p0: Point23List, p1: Point23List, p2: Point23List, p3: Point23List, u:float) -> Point2: + p0 = euclidify(p0) + p1 = euclidify(p1) + p2 = euclidify(p2) + p3 = euclidify(p3) + x = _bez03(u)*p0.x + _bez13(u)*p1.x + _bez23(u)*p2.x + _bez33(u)*p3.x y = _bez03(u)*p0.y + _bez13(u)*p1.y + _bez23(u)*p2.y + _bez33(u)*p3.y return Point2(x,y) diff --git a/solid/test/test_splines.py b/solid/test/test_splines.py index 21fcba41..e8c556b5 100755 --- a/solid/test/test_splines.py +++ b/solid/test/test_splines.py @@ -15,12 +15,14 @@ def setUp(self): Point2(1,1), Point2(2,1), ] + self.points_raw = [ (0,0), (1,1), (2,1), ] self.bezier_controls = [ Point2(0,0), Point2(1,1), Point2(2,1), Point2(2,-1), ] + self.bezier_controls_raw = [ (0,0), (1,1), (2,1), (2,-1) ] self.subdivisions = 2 def assertPointsListsEqual(self, a, b): @@ -28,23 +30,36 @@ def assertPointsListsEqual(self, a, b): self.assertEqual(str_list(a), str_list(b)) def test_catmull_rom_points(self): - expected = [Vector2(0.00, 0.00), Vector2(0.38, 0.44), Vector2(1.00, 1.00), Vector2(1.62, 1.06), Vector2(2.00, 1.00)] + expected = [Point2(0.00, 0.00), Point2(0.38, 0.44), Point2(1.00, 1.00), Point2(1.62, 1.06), Point2(2.00, 1.00)] actual = catmull_rom_points(self.points, subdivisions=self.subdivisions, close_loop=False) self.assertPointsListsEqual(expected, actual) - # actual = list((str(v) for v in actual)) - # expected = list((str(v) for v in expected)) - # self.assertEqual(expected, actual) # TODO: verify we always have the right number of points for a given call # verify that `close_loop` always behaves correctly # verify that catmull_rom_polygon() returns an OpenSCADObject # verify that start_tangent and end_tangent behavior is correct + def test_catmull_rom_points_raw(self): + # Verify that we can use raw sequences of floats as inputs (e.g [(1,2), (3.2,4)]) + # rather than sequences of Point2s + expected = [Point2(0.00, 0.00), Point2(0.38, 0.44), Point2(1.00, 1.00), Point2(1.62, 1.06), Point2(2.00, 1.00)] + actual = catmull_rom_points(self.points_raw, subdivisions=self.subdivisions, close_loop=False) + self.assertPointsListsEqual(expected, actual) + def test_bezier_points(self): expected = [Point2(0.00, 0.00), Point2(1.38, 0.62), Point2(2.00, -1.00)] actual = bezier_points(self.bezier_controls, subdivisions=self.subdivisions) self.assertPointsListsEqual(expected, actual) + def test_bezier_points_raw(self): + # Verify that we can use raw sequences of floats as inputs (e.g [(1,2), (3.2,4)]) + # rather than sequences of Point2s + expected = [Point2(0.00, 0.00), Point2(1.38, 0.62), Point2(2.00, -1.00)] + actual = bezier_points(self.bezier_controls_raw, subdivisions=self.subdivisions) + self.assertPointsListsEqual(expected, actual) + + + if __name__ == '__main__': unittest.main() \ No newline at end of file From 9ca2eda7d5c9569a003dfea98786b0243fb5490c Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 13 May 2020 18:14:49 -0500 Subject: [PATCH 16/90] Should resolve #140. Don't calculate SP version unless asked to include source code in generated file, and don't choke if we can't find the SP version --- solid/solidpython.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 900d075e..020ceaf9 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -499,10 +499,11 @@ def scad_render_to_file(scad_object: OpenSCADObject, out_dir: PathStr=None, file_header: str='', include_orig_code: bool=True) -> str: - header = "// Generated by SolidPython {version} on {date}\n".format( - version=_get_version(), - date=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - header += file_header + header = file_header + if include_orig_code: + version = _get_version() + date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + header = f"// Generated by SolidPython {version} on {date}\n" + file_header rendered_string = scad_render(scad_object, header) return _write_code_to_file(rendered_string, filepath, out_dir, include_orig_code) @@ -553,28 +554,26 @@ def _write_code_to_file(rendered_string: str, return out_path.absolute().as_posix() -def _get_version(): +def _get_version() -> str: """ Returns SolidPython version - Raises a RuntimeError if the version cannot be determined + Returns '' if no version can be found """ - + version = '' try: # if SolidPython is installed use `pkg_resources` - return pkg_resources.get_distribution('solidpython').version + version = pkg_resources.get_distribution('solidpython').version except pkg_resources.DistributionNotFound: # if the running SolidPython is not the one installed via pip, # try to read it from the project setup file version_pattern = re.compile(r"version = ['\"]([^'\"]*)['\"]") version_file_path = Path(__file__).parent.parent / 'pyproject.toml' - - version_match = version_pattern.search(version_file_path.read_text()) - if version_match: - return version_match.group(1) - - raise RuntimeError("Unable to determine software version.") - + if version_file_path.exists(): + version_match = version_pattern.search(version_file_path.read_text()) + if version_match: + version = version_match.group(1) + return version def sp_code_in_scad_comment(calling_file: PathStr) -> str: """ From da79dae479918a324bcbf901d2af48f91bd27cf5 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Thu, 14 May 2020 10:20:00 -0500 Subject: [PATCH 17/90] Resolution for #142. `splines.catmull_rom_points()` & `splines.bezier_points()` now return valid curves in 3-space, where they were limited to the XY plane before. Note that `splines.bezier_polygon()` is still limited to the XY plane because OpenSCAD's `polygon()` is limited to XY. --- solid/splines.py | 38 ++++++++++++++++++--------- solid/test/test_splines.py | 54 +++++++++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/solid/splines.py b/solid/splines.py index 6fccd576..691a3ff5 100644 --- a/solid/splines.py +++ b/solid/splines.py @@ -48,7 +48,7 @@ def catmull_rom_points( points: Sequence[Point23List], subdivisions:int = 10, close_loop: bool=False, start_tangent: Vec23 = None, - end_tangent: Vec23 = None) -> List[Point23]: + end_tangent: Vec23 = None) -> List[Point3]: """ Return a smooth set of points through `points`, with `subdivision` points between each pair of control points. @@ -61,14 +61,14 @@ def catmull_rom_points( points: Sequence[Point23List], https://www.habrador.com/tutorials/interpolation/1-catmull-rom-splines/ retrieved 20190712 """ - catmull_points: List[Point23] = [] - cat_points: List[Point23] = [] + catmull_points: List[Point3] = [] + cat_points: List[Point3] = [] # points_list = cast(List[Point23], points) - points_list = list([euclidify(p, Point2) for p in points]) + points_list = list([euclidify(p, Point3) for p in points]) if close_loop: - cat_points = [points_list[-1]] + points_list + [points_list[0]] + cat_points = euclidify([points_list[-1]] + points_list + [points_list[0]], Point3) else: # Use supplied tangents or just continue the ends of the supplied points start_tangent = start_tangent or (points_list[1] - points_list[0]) @@ -91,7 +91,7 @@ def catmull_rom_points( points: Sequence[Point23List], def _catmull_rom_segment(controls: FourPoints, subdivisions: int, - include_last=False) -> List[Point23]: + include_last=False) -> List[Point3]: """ Returns `subdivisions` Points between the 2nd & 3rd elements of `controls`, on a quadratic curve that passes through all 4 control points. @@ -107,7 +107,7 @@ def _catmull_rom_segment(controls: FourPoints, if include_last: num_points += 1 - p0, p1, p2, p3 = [euclidify(p, Point2) for p in controls] + p0, p1, p2, p3 = [euclidify(p, Point3) for p in controls] a = 2 * p1 b = p2 - p0 c = 2* p0 - 5*p1 + 4*p2 - p3 @@ -116,7 +116,7 @@ def _catmull_rom_segment(controls: FourPoints, for i in range(num_points): t = i/subdivisions pos = 0.5 * (a + (b * t) + (c * t * t) + (d * t * t * t)) - positions.append(Point2(*pos)) + positions.append(Point3(*pos)) return positions # ================== @@ -130,8 +130,19 @@ def bezier_polygon( controls: FourPoints, extrude_height:float = DEFAULT_EXTRUDE_HEIGHT, show_controls: bool = False, center: bool = True) -> OpenSCADObject: + ''' + Return an OpenSCAD representing a closed quadratic Bezier curve. + If extrude_height == 0, return a 2D `polygon()` object. + If extrude_height > 0, return a 3D extrusion of specified height. + Note that OpenSCAD won't render 2D & 3D objects together correctly, so pick + one and use that. + ''' points = bezier_points(controls, subdivisions) - shape = polygon(points) + # OpenSCAD can'ts handle Point3s in creating a polygon. Convert them to Point2s + # Note that this prevents us from making polygons outside of the XY plane, + # even though a polygon could reasonably be in some other plane while remaining 2D + points = list((Point2(p.x, p.y) for p in points)) + shape: OpenSCADObject = polygon(points) if extrude_height != 0: shape = linear_extrude(extrude_height, center=center)(shape) @@ -143,7 +154,7 @@ def bezier_polygon( controls: FourPoints, def bezier_points(controls: FourPoints, subdivisions: int = DEFAULT_SUBDIVISIONS, - include_last: bool = True) -> List[Point2]: + include_last: bool = True) -> List[Point3]: """ Returns a list of `subdivisions` (+ 1, if `include_last` is True) points on the cubic bezier curve defined by `controls`. The curve passes through @@ -158,14 +169,14 @@ def bezier_points(controls: FourPoints, # TODO: enable a smooth curve through arbitrarily many points, as described at: # https://www.algosome.com/articles/continuous-bezier-curve-line.html - points: List[Point2] = [] + points: List[Point3] = [] last_elt = 1 if include_last else 0 for i in range(subdivisions + last_elt): u = i/subdivisions points.append(_point_along_bez4(*controls, u)) return points -def _point_along_bez4(p0: Point23List, p1: Point23List, p2: Point23List, p3: Point23List, u:float) -> Point2: +def _point_along_bez4(p0: Point23List, p1: Point23List, p2: Point23List, p3: Point23List, u:float) -> Point3: p0 = euclidify(p0) p1 = euclidify(p1) p2 = euclidify(p2) @@ -173,7 +184,8 @@ def _point_along_bez4(p0: Point23List, p1: Point23List, p2: Point23List, p3: Poi x = _bez03(u)*p0.x + _bez13(u)*p1.x + _bez23(u)*p2.x + _bez33(u)*p3.x y = _bez03(u)*p0.y + _bez13(u)*p1.y + _bez23(u)*p2.y + _bez33(u)*p3.y - return Point2(x,y) + z = _bez03(u)*p0.z + _bez13(u)*p1.z + _bez23(u)*p2.z + _bez33(u)*p3.z + return Point3(x, y, z) def _bez03(u:float) -> float: return pow((1-u), 3) diff --git a/solid/test/test_splines.py b/solid/test/test_splines.py index e8c556b5..a597088c 100755 --- a/solid/test/test_splines.py +++ b/solid/test/test_splines.py @@ -3,24 +3,24 @@ import unittest from solid.test.ExpandedTestCase import DiffOutput from solid import * -from solid.splines import catmull_rom_points, bezier_points -from euclid3 import Point2, Vector2 +from solid.splines import catmull_rom_points, bezier_points, bezier_polygon +from euclid3 import Point2, Point3, Vector2, Vector3 SEGMENTS = 8 class TestSplines(DiffOutput): def setUp(self): self.points = [ - Point2(0,0), - Point2(1,1), - Point2(2,1), + Point3(0,0), + Point3(1,1), + Point3(2,1), ] self.points_raw = [ (0,0), (1,1), (2,1), ] self.bezier_controls = [ - Point2(0,0), - Point2(1,1), - Point2(2,1), - Point2(2,-1), + Point3(0,0), + Point3(1,1), + Point3(2,1), + Point3(2,-1), ] self.bezier_controls_raw = [ (0,0), (1,1), (2,1), (2,-1) ] self.subdivisions = 2 @@ -30,33 +30,55 @@ def assertPointsListsEqual(self, a, b): self.assertEqual(str_list(a), str_list(b)) def test_catmull_rom_points(self): - expected = [Point2(0.00, 0.00), Point2(0.38, 0.44), Point2(1.00, 1.00), Point2(1.62, 1.06), Point2(2.00, 1.00)] + expected = [Point3(0.00, 0.00), Point3(0.38, 0.44), Point3(1.00, 1.00), Point3(1.62, 1.06), Point3(2.00, 1.00)] actual = catmull_rom_points(self.points, subdivisions=self.subdivisions, close_loop=False) self.assertPointsListsEqual(expected, actual) # TODO: verify we always have the right number of points for a given call # verify that `close_loop` always behaves correctly - # verify that catmull_rom_polygon() returns an OpenSCADObject # verify that start_tangent and end_tangent behavior is correct def test_catmull_rom_points_raw(self): # Verify that we can use raw sequences of floats as inputs (e.g [(1,2), (3.2,4)]) # rather than sequences of Point2s - expected = [Point2(0.00, 0.00), Point2(0.38, 0.44), Point2(1.00, 1.00), Point2(1.62, 1.06), Point2(2.00, 1.00)] + expected = [Point3(0.00, 0.00), Point3(0.38, 0.44), Point3(1.00, 1.00), Point3(1.62, 1.06), Point3(2.00, 1.00)] actual = catmull_rom_points(self.points_raw, subdivisions=self.subdivisions, close_loop=False) - self.assertPointsListsEqual(expected, actual) + self.assertPointsListsEqual(expected, actual) + + def test_catmull_rom_points_3d(self): + points = [Point3(-1,-1,0), Point3(0,0,1), Point3(1,1,0)] + expected = [Point3(-1.00, -1.00, 0.00), Point3(-0.62, -0.62, 0.50), Point3(0.00, 0.00, 1.00), Point3(0.62, 0.62, 0.50), Point3(1.00, 1.00, 0.00)] + actual = catmull_rom_points(points, subdivisions=2) + self.assertPointsListsEqual(expected, actual) def test_bezier_points(self): - expected = [Point2(0.00, 0.00), Point2(1.38, 0.62), Point2(2.00, -1.00)] + expected = [Point3(0.00, 0.00), Point3(1.38, 0.62), Point3(2.00, -1.00)] actual = bezier_points(self.bezier_controls, subdivisions=self.subdivisions) self.assertPointsListsEqual(expected, actual) def test_bezier_points_raw(self): # Verify that we can use raw sequences of floats as inputs (e.g [(1,2), (3.2,4)]) # rather than sequences of Point2s - expected = [Point2(0.00, 0.00), Point2(1.38, 0.62), Point2(2.00, -1.00)] + expected = [Point3(0.00, 0.00), Point3(1.38, 0.62), Point3(2.00, -1.00)] actual = bezier_points(self.bezier_controls_raw, subdivisions=self.subdivisions) - self.assertPointsListsEqual(expected, actual) + self.assertPointsListsEqual(expected, actual) + + def test_bezier_points_3d(self): + # verify that we get a valid bezier curve back even when its control points + # are outside the XY plane and aren't coplanar + controls_3d = [Point3(-2,-1, 0), Point3(-0.5, -0.5, 1), Point3(0.5, 0.5, 1), Point3(2,1,0)] + actual = bezier_points(controls_3d, subdivisions=self.subdivisions) + expected = [Point3(-2.00, -1.00, 0.00),Point3(0.00, 0.00, 0.75), Point3(2.00, 1.00, 0.00)] + self.assertPointsListsEqual(expected, actual) + + def test_bezier_polygon(self): + # Notably, OpenSCAD won't render a polygon made of 3-tuples, even if it + # lies in the XY plane. Verify that our generated polygon() code contains + # only 2-tuples + poly = bezier_polygon(self.bezier_controls, extrude_height=0, subdivisions=self.subdivisions) + actual = all((isinstance(p, Point2) for p in poly.params['points'])) + expected = True + self.assertEqual(expected, actual) From 47ffa8cf411a150ebf7fb7868c18267efbe4fe62 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Thu, 21 May 2020 13:01:25 -0500 Subject: [PATCH 18/90] Added `splines.catmull_rom_prism()` that creates a closed polyhedron with curving sides as determined by control points. Bumped version number to 0.4.9 --- pyproject.toml | 2 +- solid/splines.py | 191 +++++++++++++++++++++++++++++++++++-- solid/test/test_splines.py | 22 ++++- 3 files changed, 203 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a31ac912..4791a55b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "0.4.8" +version = "0.4.9" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" diff --git a/solid/splines.py b/solid/splines.py index 691a3ff5..4b6d393c 100644 --- a/solid/splines.py +++ b/solid/splines.py @@ -1,16 +1,25 @@ #! /usr/bin/env python from math import pow -from solid import circle, cylinder, polygon, color, OpenSCADObject, translate, linear_extrude +from solid import union, circle, cylinder, polygon, color, OpenSCADObject, translate, linear_extrude, polyhedron from solid.utils import bounding_box, right, Red, Tuple3, euclidify from euclid3 import Vector2, Vector3, Point2, Point3 from typing import Sequence, Tuple, Union, List, cast Point23 = Union[Point2, Point3] -Point23List = Union[Point23, Tuple[float, float], Tuple[float, float, float]] +# These *Input types accept either euclid3.Point* objects, or bare n-tuples +Point2Input = Union[Point2, Tuple[float, float]] +Point3Input = Union[Point3, Tuple[float, float, float]] +Point23Input = Union[Point2Input, Point3Input] + +PointInputs = Sequence[Point23Input] + +FaceTrio = Tuple[int, int, int] +CMPatchPoints = Tuple[Sequence[Point3Input], Sequence[Point3Input]] + Vec23 = Union[Vector2, Vector3] -FourPoints = Tuple[Point23List, Point23List, Point23List, Point23List] +FourPoints = Tuple[Point23Input, Point23Input, Point23Input, Point23Input] SEGMENTS = 48 DEFAULT_SUBDIVISIONS = 10 @@ -19,7 +28,7 @@ # ======================= # = CATMULL-ROM SPLINES = # ======================= -def catmull_rom_polygon(points: Sequence[Point23List], +def catmull_rom_polygon(points: Sequence[Point23Input], subdivisions: int = DEFAULT_SUBDIVISIONS, extrude_height: float = DEFAULT_EXTRUDE_HEIGHT, show_controls: bool =False, @@ -44,13 +53,13 @@ def catmull_rom_polygon(points: Sequence[Point23List], shape += control_points(points, extrude_height, center) return shape -def catmull_rom_points( points: Sequence[Point23List], - subdivisions:int = 10, +def catmull_rom_points( points: Sequence[Point23Input], + subdivisions:int = DEFAULT_SUBDIVISIONS, close_loop: bool=False, start_tangent: Vec23 = None, end_tangent: Vec23 = None) -> List[Point3]: """ - Return a smooth set of points through `points`, with `subdivision` points + Return a smooth set of points through `points`, with `subdivisions` points between each pair of control points. If `close_loop` is False, `start_tangent` and `end_tangent` can specify @@ -119,6 +128,81 @@ def _catmull_rom_segment(controls: FourPoints, positions.append(Point3(*pos)) return positions +def catmull_rom_patch_points(patch:Tuple[PointInputs, PointInputs], + subdivisions:int = DEFAULT_SUBDIVISIONS, + index_start:int = 0) -> Tuple[List[Point3], List[FaceTrio]]: + verts: List[Point3] = [] + faces: List[FaceTrio] = [] + + cm_points_a = catmull_rom_points(patch[0], subdivisions=subdivisions) + cm_points_b = catmull_rom_points(patch[1], subdivisions=subdivisions) + + strip_length = len(cm_points_a) + + for i in range(subdivisions + 1): + frac = i/subdivisions + verts += list([affine_combination(a,b, frac) for a,b in zip(cm_points_a, cm_points_b)]) + a_start = i*strip_length + index_start + b_start = a_start + strip_length + # This connects the verts we just created to the verts we'll make on the + # next loop. So don't calculate for the last loop + if i < subdivisions: + faces += face_strip_list(a_start, b_start, strip_length) + + return verts, faces + +def catmull_rom_patch(patch:Tuple[PointInputs, PointInputs], subdivisions:int = DEFAULT_SUBDIVISIONS) -> OpenSCADObject: + + faces, vertices = catmull_rom_patch_points(patch, subdivisions) + return polyhedron(faces, vertices) + +def catmull_rom_prism( control_curves:Sequence[PointInputs], + subdivisions:int = DEFAULT_SUBDIVISIONS, + closed_ring:bool = True, + add_caps:bool = True ) -> polyhedron: + + verts: List[Point3] = [] + faces: List[FaceTrio] = [] + + curves = list([euclidify(c) for c in control_curves]) + if closed_ring: + curves.append(curves[0]) + + curve_length = (len(curves[0]) -1) * subdivisions + 1 + for i, (a, b) in enumerate(zip(curves[:-1], curves[1:])): + index_start = len(verts) - curve_length + first_new_vert = curve_length + if i == 0: + index_start = 0 + first_new_vert = 0 + + new_verts, new_faces = catmull_rom_patch_points((a,b), subdivisions=subdivisions, index_start=index_start) + + # new_faces describes all the triangles in the patch we just computed, + # but new_verts shares its first curve_length vertices with the last + # curve_length vertices; Add on only the new points + verts += new_verts[first_new_vert:] + faces += new_faces + + if closed_ring and add_caps: + bot_indices = range(0, len(verts), curve_length) + top_indices = range(curve_length-1, len(verts), curve_length) + bot_points = [verts[i] for i in bot_indices] + top_points = [verts[i] for i in top_indices] + + # FIXME: This won't work, since it assumes that the points making + # up the two end caps are all in order. In fact, that's not the case; + # the indexes of the points at the base cap are 0, 41, 82, etc. on a curve + # with 5 points and 10 subdivisions. + bot_centroid, bot_faces = centroid_endcap(bot_points, bot_indices, len(verts)) + top_centroid, top_faces = centroid_endcap(top_points, top_indices, len(verts) + 1, invert=True) + verts += [bot_centroid, top_centroid] + faces += bot_faces + faces += top_faces + + p = polyhedron(faces=faces, points=verts, convexity=3) + return p + # ================== # = BEZIER SPLINES = # ================== @@ -176,7 +260,7 @@ def bezier_points(controls: FourPoints, points.append(_point_along_bez4(*controls, u)) return points -def _point_along_bez4(p0: Point23List, p1: Point23List, p2: Point23List, p3: Point23List, u:float) -> Point3: +def _point_along_bez4(p0: Point23Input, p1: Point23Input, p2: Point23Input, p3: Point23Input, u:float) -> Point3: p0 = euclidify(p0) p1 = euclidify(p1) p2 = euclidify(p2) @@ -199,6 +283,10 @@ def _bez23(u:float) -> float: def _bez33(u:float) -> float: return pow(u,3) +# ================ +# = HOBBY CURVES = +# ================ + # =========== # = HELPERS = # =========== @@ -218,5 +306,90 @@ def control_points(points: Sequence[Point23], extrude_height:float=0, center:boo else: h = extrude_height * 1.1 c = cylinder(r=r, h=h, center=center) - controls = color(points_color)([translate([p.x, p.y])(c) for p in points]) + controls = color(points_color)([translate((p.x, p.y, 0))(c) for p in points]) return controls + +def face_strip_list(a_start:int, b_start:int, length:int, close_loop:bool=False) -> List[FaceTrio]: + # If a_start is the index of the vertex at one end of a row of points in a surface, + # and b_start is the index of the vertex at the same end of the next row of points, + # return a list of lists of indices describing faces for the whole row: + # face_strip_list(a_start = 0, b_start = 3, length=3) => [[0,3,4], [0,4,1], [1,4,5], [1,5,2]] + # 3-4-5 + # |/|/| + # 0-1-2 => [[0,3,4], [0,4,1], [1,4,5], [1,5,2]] + # + # If close_loop is true, add one more pair of faces connecting the far + # edge of the strip to the near edge, in this case [[2,5,3], [2,3,0]] + faces: List[FaceTrio] = [] + for a, b in zip(range(a_start, a_start + length-1), range(b_start, b_start + length-1)): + faces.append((a, b+1, b)) + faces.append((a, a+1, b+1)) + if close_loop: + faces.append((a+length-1, b+length-1, b)) + faces.append((a+length-1, b, a)) + return faces + +def fan_endcap_list(cap_points:int=3, index_start:int=0) -> List[FaceTrio]: + ''' + Return a face-triangles list for the endpoint of a tube with cap_points points + We construct a fan of triangles all starting at point index_start and going + to each point in turn. + + NOTE that this would not work for non-convex rings. + In that case, it would probably be better to create a new centroid point and have + all triangle reach out from it. That wouldn't handle all polygons, but would + work with mildly concave ones like a star, for example. + + So fan_endcap_list(cap_points=6, index_start=0), like so: + 0 + / \ + 5 1 + | | + 4 2 + \ / + 3 + + returns: [(0,1,2), (0,2,3), (0,3,4), (0,4,5)] + ''' + faces: List[FaceTrio] = [] + for i in range(index_start + 1, index_start + cap_points - 1): + faces.append((index_start, i, i+1)) + return faces + +def centroid_endcap(points:Sequence[Point3], indices:Sequence[int], total_vert_count:int, invert:bool = False) -> Tuple[Point3, List[FaceTrio]]: + # Given a list of points at one end of a polyhedron tube, and their + # accompanying indices, make a centroid point, and return all the triangle + # information needed to make an endcap polyhedron. + + # `total_vert_count` should be the number of vertices in the existing shape + # *before* calling this function; we'll return a point that should be appended + # to the total vertex list for the polyhedron-to-be + + # This is sufficient for some moderately concave polygonal endcaps, + # (a star shape, say), but wouldn't be enough for more irregularly convex + # polygons (anyplace where a segment from the centroid to a point on the + # polygon crosses an edge of the polygon) + faces: List[FaceTrio] = [] + center = centroid(points) + centroid_index = total_vert_count + + for a,b in zip(indices[:-1], indices[1:]): + faces.append((centroid_index, a, b)) + faces.append((centroid_index, indices[-1], indices[0])) + + if invert: + faces = list((reversed(f) for f in faces)) # type: ignore + + return (center, faces) + +def centroid(points:Sequence[Point23]) -> Point23: + total = Point3(0,0,0) + for p in points: + total += p + total /= len(points) + return total + +def affine_combination(a:Point23, b:Point23, fraction:float) -> Point23: + # Return a Point[23] between a & b, where fraction==0 => a, fraction==1 => b + return (1-fraction) * a + fraction*b + diff --git a/solid/test/test_splines.py b/solid/test/test_splines.py index a597088c..596306e5 100755 --- a/solid/test/test_splines.py +++ b/solid/test/test_splines.py @@ -3,8 +3,10 @@ import unittest from solid.test.ExpandedTestCase import DiffOutput from solid import * -from solid.splines import catmull_rom_points, bezier_points, bezier_polygon +from solid.utils import euclidify +from solid.splines import catmull_rom_points, catmull_rom_prism, bezier_points, bezier_polygon from euclid3 import Point2, Point3, Vector2, Vector3 +from math import pi SEGMENTS = 8 @@ -79,7 +81,23 @@ def test_bezier_polygon(self): actual = all((isinstance(p, Point2) for p in poly.params['points'])) expected = True self.assertEqual(expected, actual) - + + def test_catmull_rom_prism(self): + sides = 3 + UP = Vector3(0,0,1) + + control_points = [[10, 10, 0], [10, 10, 5], [8, 8, 15]] + + cat_tube = [] + angle_step = 2*pi/sides + for i in range(sides): + rotated_controls = list((euclidify(p, Point3).rotate_around(UP, angle_step*i) for p in control_points)) + cat_tube.append(rotated_controls) + + poly = catmull_rom_prism(cat_tube, self.subdivisions, closed_ring=True, add_caps=True) + actual = (len(poly.params['points']), len(poly.params['faces'])) + expected = (37, 62) + self.assertEqual(expected, actual) From a773e4606e1a7272797780bc6595350a3a9f8027 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 24 May 2020 12:44:30 -0500 Subject: [PATCH 19/90] Edit to make `polygon()` accept 2D points happily. OpenSCAD silently fails when 3D points are passed to it; project points onto XY plane before passing to OpenSCAD --- solid/objects.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index b6ca0f1c..c85744be 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -3,7 +3,7 @@ """ from pathlib import Path from types import SimpleNamespace -from typing import Dict, Optional, Sequence, Tuple, Union +from typing import Dict, Optional, Sequence, Tuple, Union, List from .solidpython import OpenSCADObject @@ -22,6 +22,9 @@ ScadSize = Union[int, Sequence[float]] OpenSCADObjectPlus = Union[OpenSCADObject, Sequence[OpenSCADObject]] +def _to_point2s(points:Points) -> List[P3]: + return list([(p[0], p[1]) for p in points]) + class polygon(OpenSCADObject): """ @@ -36,13 +39,16 @@ class polygon(OpenSCADObject): polygon has holes. The parameter is optional and if omitted the points are assumed in order. (The 'pN' components of the *paths* vector are 0-indexed references to the elements of the *points* vector.) + + NOTE: OpenSCAD accepts only 2D points for `polygon()`. Convert any 3D points + to 2D before compiling """ def __init__(self, points: Points, paths: Indexes = None) -> None: if not paths: paths = [list(range(len(points)))] super().__init__('polygon', - {'points': points, 'paths': paths}) + {'points': _to_point2s(points), 'paths': paths}) class circle(OpenSCADObject): From 7ab68ea9b38e48598e257692263cf33fd76159da Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 24 May 2020 12:47:25 -0500 Subject: [PATCH 20/90] Added a little extra documentation to splines_example.py, which was broken by recent changes to spline behavior (2D -> 3D) Now fixed with the previous change to `polygon()` --- solid/examples/splines_example.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/solid/examples/splines_example.py b/solid/examples/splines_example.py index 7896b72e..002b5049 100755 --- a/solid/examples/splines_example.py +++ b/solid/examples/splines_example.py @@ -2,7 +2,7 @@ import os import sys from solid import * -from solid.utils import Red, right, forward +from solid.utils import Red, right, forward, back from solid.splines import catmull_rom_points, catmull_rom_polygon, control_points from solid.splines import bezier_polygon, bezier_points @@ -10,13 +10,18 @@ def assembly(): # Catmull-Rom Splines - a = basic_catmull_rom() - a += forward(4)(catmull_rom_spline_variants()) - a += forward(6)(bottle_shape(width=2, height=6)) + a = basic_catmull_rom() # Top row in OpenSCAD output + a += back(4)(catmull_rom_spline_variants()) # Row 2 + a += back(12)(bottle_shape(width=2, height=6)) # Row 3, the bottle shape + + # # TODO: include examples for 3D surfaces: + # a += back(16)(catmull_rom_patches()) + # a += back(20)(catmull_rom_prism()) + # a += back(24)(catmull_rom_prism_smooth()) # Bezier Splines - a += forward(12)(basic_bezier()) - a += forward(18)(bezier_points_variants()) + a += back(16)(basic_bezier()) # Row 4 + a += back(20)(bezier_points_variants()) # Row 5 return a def basic_catmull_rom(): @@ -67,6 +72,18 @@ def catmull_rom_spline_variants(): return a +def catmull_rom_patches(): + # TODO: write this + pass + +def catmull_rom_prism(): + # TODO: write this + pass + +def catmull_rom_prism_smooth(): + # TODO: write this + pass + def bottle_shape(width: float, height: float, neck_width:float=None, neck_height:float=None): if neck_width == None: neck_width = width * 0.4 From 455a19b992ddfa6ea43d67120c3a76a631ea6124 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 24 May 2020 12:51:03 -0500 Subject: [PATCH 21/90] -- Fixed a bug in `catmull_rom_points()` that made closed catmull rom shapes C1-discontinuous. -- Added `catmull_rom_prism_smooth_edges()` and added a `smooth_edges` argument to `catmull_rom_prism()` --- solid/splines.py | 94 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/solid/splines.py b/solid/splines.py index 4b6d393c..f9e4e373 100644 --- a/solid/splines.py +++ b/solid/splines.py @@ -77,14 +77,16 @@ def catmull_rom_points( points: Sequence[Point23Input], points_list = list([euclidify(p, Point3) for p in points]) if close_loop: - cat_points = euclidify([points_list[-1]] + points_list + [points_list[0]], Point3) + cat_points = euclidify([points_list[-1]] + points_list + points_list[0:2], Point3) else: # Use supplied tangents or just continue the ends of the supplied points start_tangent = start_tangent or (points_list[1] - points_list[0]) + start_tangent = euclidify(start_tangent, Vector3) end_tangent = end_tangent or (points_list[-2] - points_list[-1]) + end_tangent = euclidify(end_tangent, Vector3) cat_points = [points_list[0]+ start_tangent] + points_list + [points_list[-1] + end_tangent] - last_point_range = len(cat_points) - 2 if close_loop else len(cat_points) - 3 + last_point_range = len(cat_points) - 3 if close_loop else len(cat_points) - 3 for i in range(0, last_point_range): include_last = True if i == last_point_range - 1 else False @@ -159,7 +161,10 @@ def catmull_rom_patch(patch:Tuple[PointInputs, PointInputs], subdivisions:int = def catmull_rom_prism( control_curves:Sequence[PointInputs], subdivisions:int = DEFAULT_SUBDIVISIONS, closed_ring:bool = True, - add_caps:bool = True ) -> polyhedron: + add_caps:bool = True, + smooth_edges: bool = False ) -> polyhedron: + if smooth_edges: + return catmull_rom_prism_smooth_edges(control_curves, subdivisions, closed_ring, add_caps) verts: List[Point3] = [] faces: List[FaceTrio] = [] @@ -187,22 +192,62 @@ def catmull_rom_prism( control_curves:Sequence[PointInputs], if closed_ring and add_caps: bot_indices = range(0, len(verts), curve_length) top_indices = range(curve_length-1, len(verts), curve_length) - bot_points = [verts[i] for i in bot_indices] - top_points = [verts[i] for i in top_indices] - - # FIXME: This won't work, since it assumes that the points making - # up the two end caps are all in order. In fact, that's not the case; - # the indexes of the points at the base cap are 0, 41, 82, etc. on a curve - # with 5 points and 10 subdivisions. - bot_centroid, bot_faces = centroid_endcap(bot_points, bot_indices, len(verts)) - top_centroid, top_faces = centroid_endcap(top_points, top_indices, len(verts) + 1, invert=True) - verts += [bot_centroid, top_centroid] + + bot_centroid, bot_faces = centroid_endcap(verts, bot_indices) + verts.append(bot_centroid) faces += bot_faces + # Note that bot_centroid must be added to verts before creating the + # top endcap; otherwise both endcaps would point to the same centroid point + top_centroid, top_faces = centroid_endcap(verts, top_indices, invert=True) + verts.append(top_centroid) faces += top_faces p = polyhedron(faces=faces, points=verts, convexity=3) return p +def catmull_rom_prism_smooth_edges( control_curves:Sequence[PointInputs], + subdivisions:int = DEFAULT_SUBDIVISIONS, + closed_ring:bool = True, + add_caps:bool = True ) -> polyhedron: + + verts: List[Point3] = [] + faces: List[FaceTrio] = [] + + # TODO: verify that each control_curve has the same length + + curves = list([euclidify(c) for c in control_curves]) + + expanded_curves = [catmull_rom_points(c, subdivisions, close_loop=False) for c in curves] + expanded_length = len(expanded_curves[0]) + for i in range(expanded_length): + contour_controls = [c[i] for c in expanded_curves] + contour = catmull_rom_points(contour_controls, subdivisions, close_loop=closed_ring) + verts += contour + + contour_length = len(contour) + # generate the face triangles between the last two rows of vertices + if i > 0: + a_start = len(verts) - 2 * contour_length + b_start = len(verts) - contour_length + new_faces = face_strip_list(a_start, b_start, length=contour_length, close_loop=closed_ring) + faces += new_faces + + if closed_ring and add_caps: + bot_indices = range(0, contour_length) + top_indices = range(len(verts) - contour_length, len(verts)) + + bot_centroid, bot_faces = centroid_endcap(verts, bot_indices) + verts.append(bot_centroid) + faces += bot_faces + # Note that bot_centroid must be added to verts before creating the + # top endcap; otherwise both endcaps would point to the same centroid point + top_centroid, top_faces = centroid_endcap(verts, top_indices, invert=True) + verts.append(top_centroid) + faces += top_faces + + p = polyhedron(faces=faces, points=verts, convexity=3) + return p + # ================== # = BEZIER SPLINES = # ================== @@ -215,7 +260,7 @@ def bezier_polygon( controls: FourPoints, show_controls: bool = False, center: bool = True) -> OpenSCADObject: ''' - Return an OpenSCAD representing a closed quadratic Bezier curve. + Return an OpenSCAD object representing a closed quadratic Bezier curve. If extrude_height == 0, return a 2D `polygon()` object. If extrude_height > 0, return a 3D extrusion of specified height. Note that OpenSCAD won't render 2D & 3D objects together correctly, so pick @@ -356,22 +401,21 @@ def fan_endcap_list(cap_points:int=3, index_start:int=0) -> List[FaceTrio]: faces.append((index_start, i, i+1)) return faces -def centroid_endcap(points:Sequence[Point3], indices:Sequence[int], total_vert_count:int, invert:bool = False) -> Tuple[Point3, List[FaceTrio]]: - # Given a list of points at one end of a polyhedron tube, and their - # accompanying indices, make a centroid point, and return all the triangle - # information needed to make an endcap polyhedron. - - # `total_vert_count` should be the number of vertices in the existing shape - # *before* calling this function; we'll return a point that should be appended - # to the total vertex list for the polyhedron-to-be - +def centroid_endcap(tube_points:Sequence[Point3], indices:Sequence[int], invert:bool = False) -> Tuple[Point3, List[FaceTrio]]: + # tube_points: all points in a polyhedron tube + # indices: the indexes of the points at the desired end of the tube + # invert: if True, invert the order of the generated faces. One endcap in + # each pair should be inverted + # + # Return all the triangle information needed to make an endcap polyhedron + # # This is sufficient for some moderately concave polygonal endcaps, # (a star shape, say), but wouldn't be enough for more irregularly convex # polygons (anyplace where a segment from the centroid to a point on the # polygon crosses an edge of the polygon) faces: List[FaceTrio] = [] - center = centroid(points) - centroid_index = total_vert_count + center = centroid([tube_points[i] for i in indices]) + centroid_index = len(tube_points) for a,b in zip(indices[:-1], indices[1:]): faces.append((centroid_index, a, b)) From f79d68c7fbb82f1763ca7de1ec24576fbfc02585 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 24 May 2020 12:55:45 -0500 Subject: [PATCH 22/90] Fixed tests for new `polygon()` behavior, sending only 2D points to OpenSCAD --- solid/test/test_solidpython.py | 2 +- solid/test/test_splines.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 02a64ad1..b7e5dd1a 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -16,7 +16,7 @@ from solid.test.ExpandedTestCase import DiffOutput scad_test_case_templates = [ - {'name': 'polygon', 'class': 'polygon' , 'kwargs': {'paths': [[0, 1, 2]]}, 'expected': '\n\npolygon(paths = [[0, 1, 2]], points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]]}, }, + {'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]]}, }, {'name': 'circle', 'class': 'circle' , 'kwargs': {'segments': 12, 'r': 1}, 'expected': '\n\ncircle($fn = 12, r = 1);', 'args': {}, }, {'name': 'circle_diam', 'class': 'circle' , 'kwargs': {'segments': 12, 'd': 1}, 'expected': '\n\ncircle($fn = 12, d = 1);', 'args': {}, }, {'name': 'square', 'class': 'square' , 'kwargs': {'center': False, 'size': 1}, 'expected': '\n\nsquare(center = false, size = 1);', 'args': {}, }, diff --git a/solid/test/test_splines.py b/solid/test/test_splines.py index 596306e5..10641044 100755 --- a/solid/test/test_splines.py +++ b/solid/test/test_splines.py @@ -73,15 +73,6 @@ def test_bezier_points_3d(self): expected = [Point3(-2.00, -1.00, 0.00),Point3(0.00, 0.00, 0.75), Point3(2.00, 1.00, 0.00)] self.assertPointsListsEqual(expected, actual) - def test_bezier_polygon(self): - # Notably, OpenSCAD won't render a polygon made of 3-tuples, even if it - # lies in the XY plane. Verify that our generated polygon() code contains - # only 2-tuples - poly = bezier_polygon(self.bezier_controls, extrude_height=0, subdivisions=self.subdivisions) - actual = all((isinstance(p, Point2) for p in poly.params['points'])) - expected = True - self.assertEqual(expected, actual) - def test_catmull_rom_prism(self): sides = 3 UP = Vector3(0,0,1) From bb341d7967bbebaa771d20211abc9b4ea8ec36b9 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 26 May 2020 12:07:40 -0500 Subject: [PATCH 23/90] Inverted some face specification order in `face_strip_list()` so all faces generated in `catmull_rom_prism_smooth_edges()` are facing the same direction --- solid/splines.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/solid/splines.py b/solid/splines.py index f9e4e373..ef4310f4 100644 --- a/solid/splines.py +++ b/solid/splines.py @@ -229,7 +229,11 @@ def catmull_rom_prism_smooth_edges( control_curves:Sequence[PointInputs], if i > 0: a_start = len(verts) - 2 * contour_length b_start = len(verts) - contour_length - new_faces = face_strip_list(a_start, b_start, length=contour_length, close_loop=closed_ring) + # Note the b_start, a_start order here. This makes sure our faces + # are pointed outwards for the test cases I ran. I think if control + # curves were specified clockwise rather than counter-clockwise, all + # of the faces would be pointed inwards + new_faces = face_strip_list(b_start, a_start, length=contour_length, close_loop=closed_ring) faces += new_faces if closed_ring and add_caps: @@ -358,20 +362,22 @@ def face_strip_list(a_start:int, b_start:int, length:int, close_loop:bool=False # If a_start is the index of the vertex at one end of a row of points in a surface, # and b_start is the index of the vertex at the same end of the next row of points, # return a list of lists of indices describing faces for the whole row: - # face_strip_list(a_start = 0, b_start = 3, length=3) => [[0,3,4], [0,4,1], [1,4,5], [1,5,2]] + # face_strip_list(a_start = 0, b_start = 3, length=3) => [[0,4,3], [0,1,4], [1,5,4], [1,2,5]] # 3-4-5 # |/|/| - # 0-1-2 => [[0,3,4], [0,4,1], [1,4,5], [1,5,2]] + # 0-1-2 => [[0,4,3], [0,1,4], [1,5,4], [1,2,5]] # # If close_loop is true, add one more pair of faces connecting the far - # edge of the strip to the near edge, in this case [[2,5,3], [2,3,0]] + # edge of the strip to the near edge, in this case [[2,3,5], [2,0,3]] faces: List[FaceTrio] = [] - for a, b in zip(range(a_start, a_start + length-1), range(b_start, b_start + length-1)): + loop = length - 1 + + for a, b in zip(range(a_start, a_start + loop), range(b_start, b_start + loop)): faces.append((a, b+1, b)) faces.append((a, a+1, b+1)) if close_loop: - faces.append((a+length-1, b+length-1, b)) - faces.append((a+length-1, b, a)) + faces.append((a+loop, b, b+loop)) + faces.append((a+loop, a, b)) return faces def fan_endcap_list(cap_points:int=3, index_start:int=0) -> List[FaceTrio]: From fa8f4570ab78e33336312fe39b61890458175f03 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 6 Jun 2020 09:49:33 -0500 Subject: [PATCH 24/90] -- Rewrote `solid.utils.euclidify()` for greater clarity, and for appropriate downgrading from 3D types to 2D types; this didn't work before -- Added `closed` argument to solid.utils.offet_points, which should resolve #145. -- Added `solid.utils.path_2d()` & `solid.utils.path_2d_polygon()` to generate 2D paths of specified width along a set of points. This should be useful for 2D designs like circuit board traces, as suggested in #144. -- Tests for all changes --- solid/test/test_utils.py | 43 +++++++++++++ solid/utils.py | 127 +++++++++++++++++++++++++++------------ 2 files changed, 130 insertions(+), 40 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index cf6116a2..168e2599 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -10,6 +10,7 @@ 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 +from solid.utils import path_2d, path_2d_polygon from solid.utils import FORWARD_VEC, RIGHT_VEC, UP_VEC from solid.utils import back, down, forward, left, right, up from solid.utils import label @@ -38,6 +39,7 @@ ('euclidify_recursive', euclidify, [[[0, 0, 0], [1, 0, 0]]], '[Vector3(0.00, 0.00, 0.00), Vector3(1.00, 0.00, 0.00)]'), ('euclidify_Vector', euclidify, [Vector3(0, 0, 0)], 'Vector3(0.00, 0.00, 0.00)'), ('euclidify_recursive_Vector', euclidify, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[Vector3(0.00, 0.00, 0.00), Vector3(0.00, 0.00, 1.00)]'), + ('euclidify_3_to_2', euclidify, [Point3(0,1,2), Point2], 'Point2(0.00, 1.00)'), ('euc_to_arr', euc_to_arr, [Vector3(0, 0, 0)], '[0, 0, 0]'), ('euc_to_arr_recursive', euc_to_arr, [[Vector3(0, 0, 0), Vector3(0, 0, 1)]], '[[0, 0, 0], [0, 0, 1]]'), ('euc_to_arr_arr', euc_to_arr, [[0, 0, 0]], '[0, 0, 0]'), @@ -100,6 +102,47 @@ def test_fillet_2d_remove(self): self.assertEqualNoWhitespace(expected, actual) + def test_euclidify_non_mutating(self): + base_tri = [Point2(0, 0), Point2(10, 0), Point2(0, 10)] + next_tri = euclidify(base_tri, Point2) + expected = 3 + actual = len(base_tri) + self.assertEqual(expected, actual, 'euclidify should not mutate its arguments') + + def test_offset_points_closed(self): + actual = euc_to_arr(offset_points(tri, offset=1, closed=True)) + expected = [[1.0, 1.0], [7.585786437626904, 1.0], [1.0, 7.585786437626905]] + self.assertEqual(expected, actual) + + def test_offset_points_open(self): + actual = euc_to_arr(offset_points(tri, offset=1, closed=False)) + expected = [[0.0, 1.0], [7.585786437626904, 1.0], [-0.7071067811865479, 9.292893218813452]] + self.assertEqual(expected, actual) + + def test_path_2d(self): + base_tri = [Point2(0, 0), Point2(10, 0), Point2(10, 10)] + actual = euc_to_arr(path_2d(base_tri, width=2, closed=False)) + expected = [ + [0.0, 1.0], [9.0, 1.0], [9.0, 10.0], + [11.0, 10.0], [11.0, -1.0], [0.0, -1.0] + ] + self.assertEqual(expected, actual) + + def test_path_2d_polygon(self): + base_tri = [Point2(0, 0), Point2(10, 0), Point2(10, 10), Point2(0,10)] + poly = path_2d_polygon(base_tri, width=2, closed=True) + expected = [ + (1.0, 1.0), (9.0, 1.0), (9.0, 9.0), (1.0, 9.0), + (-1.0, 11.0), (11.0, 11.0), (11.0, -1.0), (-1.0, -1.0) + ] + actual = euc_to_arr(poly.params['points']) + self.assertEqual(expected, actual) + + # Make sure the inner and outer paths in the polygon are disjoint + expected = [[0,1,2,3],[4,5,6,7]] + actual = poly.params['paths'] + self.assertEqual(expected, actual) + # def test_offset_points_inside(self): # expected = '' # actual = scad_render(offset_points(tri2d, offset=2, internal=True)) diff --git a/solid/utils.py b/solid/utils.py index 22356cb6..0eaae5af 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -2,7 +2,7 @@ from itertools import zip_longest from math import pi, ceil, floor, sqrt, atan2, degrees, radians -from solid import union, cube, translate, rotate, square, circle, polyhedron +from solid import union, cube, translate, rotate, square, circle, polyhedron, polygon from solid import difference, intersection, multmatrix, cylinder, color from solid import text, linear_extrude, resize from solid import run_euclid_patch @@ -17,7 +17,7 @@ # ========== # = TYPING = # ========== -from typing import Union, Tuple, Sequence, List, Optional, Callable, Dict, cast +from typing import Any, Union, Tuple, Sequence, List, Optional, Callable, Dict, cast Point23 = Union[Point2, Point3] Vector23 = Union[Vector2, Vector3] Line23 = Union[Line2, Line3] @@ -704,39 +704,49 @@ def label(a_str:str, width:float=15, halign:str="left", valign:str="baseline", # ================== # = PyEuclid Utils = # ================== -def euclidify(an_obj:EucOrTuple, - intended_class=Vector3) -> Union[Point23, Vector23]: - # If an_obj is an instance of the appropriate PyEuclid class, - # return it. Otherwise, try to turn an_obj into the appropriate - # class and throw an exception on failure - - # Since we often want to convert an entire array - # of objects (points, etc.) accept arrays of arrays - - ret = an_obj - - # See if this is an array of arrays. If so, convert all sublists - if isinstance(an_obj, (list, tuple)): - if isinstance(an_obj[0], (list, tuple)): - ret = [intended_class(*p) for p in an_obj] - elif isinstance(an_obj[0], intended_class): - # this array is already euclidified; return it - ret = an_obj - else: - try: - ret = intended_class(*an_obj) - except: - raise TypeError("Object: %s ought to be PyEuclid class %s or " - "able to form one, but is not." - % (an_obj, intended_class.__name__)) - elif not isinstance(an_obj, intended_class): - try: - ret = intended_class(*an_obj) - except: - raise TypeError("Object: %s ought to be PyEuclid class %s or " - "able to form one, but is not." - % (an_obj, intended_class.__name__)) - return ret # type: ignore +def euclidify(an_obj:EucOrTuple, intended_class:type=Vector3) -> Union[Point23, Vector23, List[Union[Point23, Vector23]]]: + ''' + Accept an object or list of objects of any relevant type (2-tuples, 3-tuples, Vector2/3, Point2/3) + and return one or more euclid3 objects of intended_class. + + # -- 3D input has its z-values dropped when intended_class is 2D + # -- 2D input has its z-values set to 0 when intended_class is 3D + + The general idea is to take in data in whatever form is handy to users + and return euclid3 types with vector math capabilities + ''' + sequence = (list, tuple) + euclidable = (list, tuple, Vector2, Vector3, Point2, Point3) + numeric = (int, float) + # If this is a list of lists, return a list of euclid objects + if isinstance(an_obj, sequence) and isinstance(an_obj[0], euclidable): + return list((_euc_obj(ao, intended_class) for ao in an_obj)) + elif isinstance(an_obj, euclidable): + return _euc_obj(an_obj, intended_class) + else: + raise TypeError(f'''Object: {an_obj} ought to be PyEuclid class + {intended_class.__name__} or able to form one, but is not.''') + +def _euc_obj(an_obj: Any, intended_class:type=Vector3) -> Union[Point23, Vector23]: + ''' Take a single object (not a list of them!) and return a euclid type + # If given a euclid obj, return the desired type, + # -- 3d types are projected to z=0 when intended_class is 2D + # -- 2D types are projected to z=0 when intended class is 3D + _euc_obj( Vector3(0,1,2), Vector3) -> Vector3(0,1,2) + _euc_obj( Vector3(0,1,2), Point3) -> Point3(0,1,2) + _euc_obj( Vector2(0,1), Vector3) -> Vector3(0,1,0) + _euc_obj( Vector2(0,1), Point3) -> Point3(0,1,0) + _euc_obj( (0,1), Vector3) -> Vector3(0,1,0) + _euc_obj( (0,1), Point3) -> Point3(0,1,0) + _euc_obj( (0,1), Point2) -> Point2(0,1,0) + _euc_obj( (0,1,2), Point2) -> Point2(0,1) + _euc_obj( (0,1,2), Point3) -> Point3(0,1,2) + ''' + elts_in_constructor = 3 + if intended_class in (Point2, Vector2): + elts_in_constructor = 2 + result = intended_class(*an_obj[:elts_in_constructor]) + return result def euc_to_arr(euc_obj_or_list: EucOrTuple) -> List[float]: # Inverse of euclidify() # Call as_arr on euc_obj_or_list or on all its members if it's a list @@ -902,7 +912,8 @@ def offset_point(a:Point2, b:Point2, c:Point2, offset:float, direction:Direction def offset_points(points:Sequence[Point23], offset:float, - internal:bool=True) -> List[Point2]: + internal:bool=True, + closed=True) -> List[Point2]: """ Given a set of points, return a set of points offset by `offset`, in the direction specified by `internal`. @@ -923,7 +934,9 @@ def offset_points(points:Sequence[Point23], """ # Note that we could just call offset_point() repeatedly, but we'd do # a lot of repeated calculations that way - src_points = list((Point2(p.x, p.y) for p in (*points, points[0]))) + src_points = euclidify(points, Point2) + if closed: + src_points.append(src_points[0]) vecs = vectors_between_points(src_points) direction = direction_of_bend(*src_points[:3]) @@ -937,9 +950,13 @@ def offset_points(points:Sequence[Point23], lines.append(Line2(a+perp, b+perp)) intersections = list((a.intersect(b) for a,b in zip(lines[:-1], lines[1:]))) - # First point is determined by intersection of first and last lines - intersections.insert(0, lines[0].intersect(lines[-1])) - + if closed: + # First point is determined by intersection of first and last lines + intersections.insert(0, lines[0].intersect(lines[-1])) + else: + # otherwise use first and last points in lines + intersections.insert(0, lines[0].p) + intersections.append(lines[-1].p + lines[-1].v) return intersections # ================== @@ -1045,6 +1062,36 @@ def _widen_angle_for_fillet(start_degrees:float, end_degrees:float) -> Tuple[flo epsilon_degrees = 0.1 return start_degrees - epsilon_degrees, end_degrees + epsilon_degrees +# ============== +# = 2D DRAWING = +# ============== +def path_2d(points:Sequence[Point23], width:float=1, closed:bool=False) -> List[Point2]: + ''' + Return a set of points describing a path of width `width` around `points`, + suitable for use as a polygon(). + + Note that if `closed` is True, the polygon will have a hole in it, meaning + that `polygon()` would need to specify its `paths` argument. Assuming 3 elements + in the original `points` list, we'd have to call: + path_points = path_2d(points, closed=True) + poly = polygon(path_points, paths=[[0,1,2],[3,4,5]]) + + Or, you know, just call `path_2d_polygon()` and let it do that for you + ''' + p_a = offset_points(points, offset=width/2, internal=True, closed=closed) + p_b = list(reversed(offset_points(points, offset=width/2, internal=False, closed=closed))) + return p_a + p_b + +def path_2d_polygon(points:Sequence[Point23], width:float=1, closed:bool=False) -> polygon: + ''' + Return an OpenSCAD `polygon()` in an area `width` units wide around `points` + ''' + path_points = path_2d(points, width, closed) + paths = list(range(len(path_points))) + if closed: + paths = [list(range(len(points))), list(range(len(points), len(path_points)))] + return polygon(path_points, paths=paths) + # ========================== # = Extrusion along a path = # ========================== From fd997064a35080831629036258c697b017b260b9 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 6 Jun 2020 10:36:54 -0500 Subject: [PATCH 25/90] Fixed bug in solid.utils.path_2d_polygon; it didn't render when `closed` was False --- solid/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solid/utils.py b/solid/utils.py index 0eaae5af..cc6327c1 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -1087,7 +1087,7 @@ def path_2d_polygon(points:Sequence[Point23], width:float=1, closed:bool=False) Return an OpenSCAD `polygon()` in an area `width` units wide around `points` ''' path_points = path_2d(points, width, closed) - paths = list(range(len(path_points))) + paths = [list(range(len(path_points)))] if closed: paths = [list(range(len(points))), list(range(len(points), len(path_points)))] return polygon(path_points, paths=paths) From f0785288398af05f931d045e21c15cfc8bcfaa15 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 6 Jun 2020 10:39:00 -0500 Subject: [PATCH 26/90] Version bump to v1.0.0; It's been 8+ years, and a full version number is long overdue --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4791a55b..d82575f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "0.4.9" +version = "1.0.0" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From 60a629658595371a9670f1be0fe960536a68defc Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 29 Jun 2020 09:54:38 -0500 Subject: [PATCH 27/90] Resolves #147. --- solid/objects.py | 4 ++-- solid/test/test_solidpython.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index c85744be..0476fe09 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -415,9 +415,9 @@ class offset(OpenSCADObject): def __init__(self, r: float = None, delta: float = None, chamfer: bool = False, segments: int=None) -> None: - if r: + if r is not None: kwargs = {'r': r} - elif delta: + elif delta is not None: kwargs = {'delta': delta, 'chamfer': chamfer} else: raise ValueError("offset(): Must supply r or delta") diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index b7e5dd1a..299b28ac 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -40,6 +40,7 @@ {'name': 'offset', 'class': 'offset' , 'kwargs': {'r': 1}, 'expected': '\n\noffset(r = 1);', 'args': {}, }, {'name': 'offset_segments', 'class': 'offset' , 'kwargs': {'r': 1, 'segments': 12}, 'expected': '\n\noffset($fn = 12, r = 1);', 'args': {}, }, {'name': 'offset_chamfer', 'class': 'offset' , 'kwargs': {'delta': 1}, 'expected': '\n\noffset(chamfer = false, delta = 1);', 'args': {}, }, + {'name': 'offset_zero_delta', 'class': 'offset' , 'kwargs': {'r': 0}, 'expected': '\n\noffset(r = 0);', 'args': {}, }, {'name': 'hull', 'class': 'hull' , 'kwargs': {}, 'expected': '\n\nhull();', 'args': {}, }, {'name': 'render', 'class': 'render' , 'kwargs': {'convexity': None}, 'expected': '\n\nrender();', 'args': {}, }, {'name': 'projection', 'class': 'projection' , 'kwargs': {'cut': None}, 'expected': '\n\nprojection();', 'args': {}, }, From a7c31d0e81f4a48225c34246ca1e20dc571e64dc Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 25 Jul 2020 18:25:49 -0500 Subject: [PATCH 28/90] in `solid.utils.extrude_along_path()`, cast `scale_factor` to float, so that even if the function receives numpy data for `scale_factors`, it will still work --- solid/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solid/utils.py b/solid/utils.py index cc6327c1..87309e32 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -1120,7 +1120,7 @@ def extrude_along_path( shape_pts:Points, for which_loop in range(len(path_pts)): path_pt = path_pts[which_loop] - scale = scale_factors[which_loop] + scale = float(scale_factors[which_loop]) # calculate the tangent to the curve at this point if which_loop > 0 and which_loop < len(path_pts) - 1: From a3f100383f62062d03053cec9b2b86dd63607cb5 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 25 Jul 2020 18:40:47 -0500 Subject: [PATCH 29/90] Added test for `solid.utils.extrude_along_path()` numpy scaling behavior. --- solid/test/test_utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 168e2599..32332a5e 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -147,6 +147,22 @@ def test_path_2d_polygon(self): # expected = '' # actual = scad_render(offset_points(tri2d, offset=2, internal=True)) # self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path_numpy(self): + try: + import numpy as np + except ImportError: + return + + N = 3 + thetas=np.linspace(0,np.pi,N) + path=list(zip(3*np.sin(thetas),3*np.cos(thetas),thetas)) + profile=list(zip(np.sin(thetas),np.cos(thetas), [0]*len(thetas))) + scalepts=list(np.linspace(1,.1,N)) + + # in earlier code, this would have thrown an exception + a = extrude_along_path(shape_pts=profile, path_pts=path, scale_factors=scalepts) + def test_label(self): expected = 'translate(v=[0,5.0000000000,0]){resize(newsize=[15,0,0.5000000000]){union(){translate(v=[0,0.0000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="Hello,",valign="baseline");}}translate(v=[0,-11.5000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="World",valign="baseline");}}}}}' From 3140d09f3d7f91a1f3ef72bcc125235b74827b1e Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 25 Jul 2020 18:49:16 -0500 Subject: [PATCH 30/90] v1.0.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d82575f1..a678baa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.0.0" +version = "1.0.1" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From 9c7a308628b791b36b3bc3693caba60b587db2b3 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 4 Aug 2020 17:49:07 -0500 Subject: [PATCH 31/90] -- Refinements on #110. import_scad() and use()/include() now read from OpenSCAD's default import paths; if you have installed OpenSCAD modules so they're usable in OpenSCAD, they should be usable in SolidPython -- Added tests for new import behavior -- Added documentation for new import behavior --- README.rst | 6 +- solid/objects.py | 116 +++++++++++++++++++++------------ solid/test/test_solidpython.py | 14 ++++ 3 files changed, 94 insertions(+), 42 deletions(-) diff --git a/README.rst b/README.rst index 8142037a..1f7e1e14 100644 --- a/README.rst +++ b/README.rst @@ -168,7 +168,8 @@ Using SolidPython Importing OpenSCAD code ======================= -- Use ``solid.import_scad(path)`` to import OpenSCAD code. +- Use ``solid.import_scad(path)`` to import OpenSCAD code. Relative paths will +check current location designated `OpenSCAD library directories `. **Ex:** @@ -197,7 +198,8 @@ Importing OpenSCAD code from solid import * # MCAD is OpenSCAD's most common utility library: https://github.com/openscad/MCAD - mcad = import_scad('/path/to/MCAD') + # If it's installed for OpenSCAD (on MacOS, at: ``$HOME/Documents/OpenSCAD/libraries``) + mcad = import_scad('MCAD') # MCAD contains about 15 separate packages, each included as its own namespace print(dir(mcad)) # => ['bearing', 'bitmap', 'boxes', etc...] diff --git a/solid/objects.py b/solid/objects.py index 0476fe09..86535bd1 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -22,7 +22,7 @@ ScadSize = Union[int, Sequence[float]] OpenSCADObjectPlus = Union[OpenSCADObject, Sequence[OpenSCADObject]] -def _to_point2s(points:Points) -> List[P3]: +def _to_point2s(points:Points) -> List[P2]: return list([(p[0], p[1]) for p in points]) @@ -747,50 +747,86 @@ def disable(openscad_obj: OpenSCADObject) -> OpenSCADObject: # =========================== # = IMPORTING OPENSCAD CODE = # =========================== -def import_scad(scad_filepath: PathStr) -> Optional[SimpleNamespace]: +def import_scad(scad_file_or_dir: PathStr) -> SimpleNamespace: + ''' + Recursively look in current directory & OpenSCAD library directories for + OpenSCAD files. Create Python mappings for all OpenSCAD modules & functions + Return a namespace or raise ValueError if no scad files found + ''' + scad = Path(scad_file_or_dir) + candidates: List[Path] = [scad] + if not scad.is_absolute(): + candidates = [d/scad for d in _openscad_library_paths()] + + for candidate_path in candidates: + namespace = _import_scad(candidate_path) + if namespace is not None: + return namespace + raise ValueError(f'Could not find .scad files at or under {scad}. \nLocations searched were: {candidates}') + +def _import_scad(scad: Path) -> Optional[SimpleNamespace]: + ''' + cases: + single scad file: + return a namespace populated with `use()` + directory + recurse into all subdirectories and *.scad files + return namespace if scad files are underneath, otherwise None + non-scad file: + return None + ''' + namespace: Optional[SimpleNamespace] = None + if scad.is_file() and scad.suffix == '.scad': + namespace = SimpleNamespace() + use(scad.absolute(), dest_namespace_dict=namespace.__dict__) + elif scad.is_dir(): + subspaces = [(f, _import_scad(f)) for f in scad.iterdir() if f.is_dir() or f.suffix == '.scad'] + for f, subspace in subspaces: + if subspace: + if namespace is None: + namespace = SimpleNamespace() + # Add a subspace to namespace named by the file/dir it represents + setattr(namespace, f.stem, subspace) + + return namespace + +def _openscad_library_paths() -> List[Path]: """ - import_scad() is the namespaced, more Pythonic way to import OpenSCAD code. - Return a python namespace containing all imported SCAD modules + Return system-dependent OpenSCAD library paths or paths defined in os.environ['OPENSCADPATH'] + """ + import platform + import os + import re - If scad_filepath is a single .scad file, all modules will be imported, - e.g. - motors = solid.import_scad(' [_stepper_motor_mount', 'stepper_motor_mount'] + paths = [Path('.')] - If scad_filepath is a directory, recursively import all scad files below - the directory and subdirectories within it. - e.g. - mcad = solid.import_scad(' ['bearing', 'boxes', 'constants', 'curves',...] - dir(mcad.bearing) # => ['bearing', 'bearingDimensions', ...] - """ - scad = Path(scad_filepath) + user_path = os.environ.get('OPENSCADPATH') + if user_path: + for s in re.split(r'\s*[;:]\s*', user_path): + paths.append(Path(s)) - namespace: Optional[SimpleNamespace] = SimpleNamespace() - scad_found = False + default_paths = { + 'Linux': Path.home() / '.local/share/OpenSCAD/libraries', + 'Darwin': Path.home() / 'Documents/OpenSCAD/libraries', + 'Windows': Path('My Documents\OpenSCAD\libraries') + } - if scad.is_file(): - scad_found = True - use(scad.absolute().as_posix(), dest_namespace_dict=namespace.__dict__) - elif scad.is_dir(): - for f in scad.glob('*.scad'): - subspace = import_scad(f.absolute().as_posix()) - setattr(namespace, f.stem, subspace) - scad_found = True - - # recurse through subdirectories, adding namespaces only if they have - # valid scad code under them. - subdirs = list([d for d in scad.iterdir() if d.is_dir()]) - for subd in subdirs: - subspace = import_scad(subd.absolute().as_posix()) - if subspace is not None: - setattr(namespace, subd.stem, subspace) - scad_found = True - - namespace = namespace if scad_found else None - return namespace + paths.append(default_paths[platform.system()]) + return paths + +def _find_library(library_name: PathStr) -> Path: + result = Path(library_name) + if not result.is_absolute(): + paths = _openscad_library_paths() + for p in paths: + f = p / result + # print(f'Checking {f} -> {f.exists()}') + if f.exists(): + result = f + return result + # use() & include() mimic OpenSCAD's use/include mechanics. # -- use() makes methods in scad_file_path.scad available to be called. # --include() makes those methods available AND executes all code in @@ -807,7 +843,7 @@ def use(scad_file_path: PathStr, use_not_include: bool = True, dest_namespace_di from .solidpython import new_openscad_class_str from .solidpython import calling_module - scad_file_path = Path(scad_file_path) + scad_file_path = _find_library(scad_file_path) contents = None try: @@ -837,6 +873,6 @@ def use(scad_file_path: PathStr, use_not_include: bool = True, dest_namespace_di return True - def include(scad_file_path: PathStr) -> bool: return use(scad_file_path, use_not_include=False) + diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 299b28ac..ae3bed60 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -205,6 +205,20 @@ def test_import_scad(self): expected = f"{header}\nuse <{abs_path}>\n\n\nsteps(howmany = 3);" self.assertEqual(expected, actual) + # Make sure we throw ValueError on nonexistent imports + 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')) + + # TODO: we should test that: + # A) scad files in the designated OpenSCAD library directories + # (path-dependent, see: solid.objects._openscad_library_paths()) + # are imported correctly. Not sure how to do this without writing + # temp files to those directories. Seems like overkill for the moment + def test_use_reserved_words(self): scad_str = '''module reserved_word_arg(or=3){\n\tcube(or);\n}\nmodule or(arg=3){\n\tcube(arg);\n}\n''' From f15d0ce6ba9c960543b71ef41adbc95c88158279 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 4 Aug 2020 17:51:34 -0500 Subject: [PATCH 32/90] bump: v1.0.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a678baa4..d79dfc86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.0.1" +version = "1.0.2" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From 2e2cde20d1c54d148f9722622f65314585cd3fff Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 11 Nov 2020 19:21:05 -0600 Subject: [PATCH 33/90] one empty line between functions, two lines after classes --- solid/solidpython.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 020ceaf9..10b277c6 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -400,7 +400,6 @@ def _find_include_strings(obj: Union[IncludedOpenSCADObject, OpenSCADObject]) -> include_strings.update(_find_include_strings(child)) return include_strings - def scad_render(scad_object: OpenSCADObject, file_header: str = '') -> str: # Make this object the root of the tree root = scad_object @@ -418,7 +417,6 @@ def scad_render(scad_object: OpenSCADObject, file_header: str = '') -> str: return file_header + includes + scad_body - def scad_render_animated(func_to_animate: AnimFunc, steps: int =20, back_and_forth: bool=True, @@ -481,7 +479,6 @@ def scad_render_animated(func_to_animate: AnimFunc, f"}}\n" return rendered_string - def scad_render_animated_file(func_to_animate:AnimFunc, steps: int=20, back_and_forth: bool=True, @@ -553,7 +550,6 @@ def _write_code_to_file(rendered_string: str, out_path.write_text(rendered_string) return out_path.absolute().as_posix() - def _get_version() -> str: """ Returns SolidPython version @@ -607,7 +603,6 @@ def extract_callable_signatures(scad_file_path: PathStr) -> List[dict]: scad_code_str = Path(scad_file_path).read_text() return parse_scad_callables(scad_code_str) - def parse_scad_callables(scad_code_str: str) -> List[dict]: callables = [] @@ -655,7 +650,6 @@ def parse_scad_callables(scad_code_str: str) -> List[dict]: return callables - def calling_module(stack_depth: int = 2) -> ModuleType: """ Returns the module *2* back in the frame stack. That means: @@ -678,7 +672,6 @@ def calling_module(stack_depth: int = 2) -> ModuleType: import __main__ as calling_mod # type: ignore return calling_mod - def new_openscad_class_str(class_name: str, args: Sequence[str] = None, kwargs: Sequence[str] = None, @@ -731,7 +724,6 @@ def new_openscad_class_str(class_name: str, return result - def _subbed_keyword(keyword: str) -> str: """ Append an underscore to any python reserved word. @@ -744,7 +736,6 @@ def _subbed_keyword(keyword: str) -> str: f"can be accessed with `{new_key}` in SolidPython\n") return new_key - def _unsubbed_keyword(subbed_keyword: str) -> str: """ Remove trailing underscore for already-subbed python reserved words. @@ -753,11 +744,9 @@ def _unsubbed_keyword(subbed_keyword: str) -> str: shortened = subbed_keyword[:-1] return shortened if shortened in PYTHON_ONLY_RESERVED_WORDS else subbed_keyword - # now that we have the base class defined, we can do a circular import from . import objects - def py2openscad(o: Union[bool, float, str, Iterable]) -> str: if type(o) == bool: return str(o).lower() @@ -780,6 +769,5 @@ def py2openscad(o: Union[bool, float, str, Iterable]) -> str: return s return str(o) - def indent(s: str) -> str: return s.replace("\n", "\n\t") From 126a0728f29282d20f0afdf3aaddbf1a2bb4b4ce Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 16 Nov 2020 08:43:04 -0600 Subject: [PATCH 34/90] -- polygon() doesn't supply paths argument by default; OpenSCAD takes care of this manually -- polygon() allows points to be passed in as an IncludedSCADObject --- solid/objects.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index 86535bd1..2a4d2019 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -5,7 +5,7 @@ from types import SimpleNamespace from typing import Dict, Optional, Sequence, Tuple, Union, List -from .solidpython import OpenSCADObject +from .solidpython import IncludedOpenSCADObject, OpenSCADObject PathStr = Union[Path, str] @@ -22,10 +22,6 @@ ScadSize = Union[int, Sequence[float]] OpenSCADObjectPlus = Union[OpenSCADObject, Sequence[OpenSCADObject]] -def _to_point2s(points:Points) -> List[P2]: - return list([(p[0], p[1]) for p in points]) - - class polygon(OpenSCADObject): """ Create a polygon with the specified points and paths. @@ -44,11 +40,18 @@ class polygon(OpenSCADObject): to 2D before compiling """ - def __init__(self, points: Points, paths: Indexes = None) -> None: - if not paths: - paths = [list(range(len(points)))] - super().__init__('polygon', - {'points': _to_point2s(points), 'paths': paths}) + def __init__(self, points: Union[Points, IncludedOpenSCADObject], paths: Indexes = None) -> None: + # Force points to 2D if they're defined in Python, pass through if they're + # included OpenSCAD code + pts = points # type: ignore + if not isinstance(points, IncludedOpenSCADObject): + pts = list([(p[0], p[1]) for p in points]) # type: ignore + + args = {'points':pts} + # If not supplied, OpenSCAD assumes all points in order for paths + if paths: + args['paths'] = paths # type: ignore + super().__init__('polygon', args) class circle(OpenSCADObject): From 076ab2d7e6434c76ce94b878492606c4814e0a3b Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 16 Nov 2020 08:44:37 -0600 Subject: [PATCH 35/90] Accept IncludedOpenSCADObject for all arguments to OpenSCADObjects. This allows us to accept calculations made in imported OpenSCAD as well as native Python ones --- solid/solidpython.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/solid/solidpython.py b/solid/solidpython.py index 10b277c6..07cb3549 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -398,6 +398,11 @@ def _find_include_strings(obj: Union[IncludedOpenSCADObject, OpenSCADObject]) -> include_strings.add(obj.include_string) for child in obj.children: include_strings.update(_find_include_strings(child)) + # We also accept IncludedOpenSCADObject instances as parameters to functions, + # so search in obj.params as well + for param in obj.params.values(): + if isinstance(param, OpenSCADObject): + include_strings.update(_find_include_strings(param)) return include_strings def scad_render(scad_object: OpenSCADObject, file_header: str = '') -> str: @@ -757,6 +762,8 @@ def py2openscad(o: Union[bool, float, str, Iterable]) -> str: if type(o).__name__ == "ndarray": import numpy # type: ignore return numpy.array2string(o, separator=",", threshold=1000000000) + if isinstance(o, IncludedOpenSCADObject): + return o._render()[1:-1] if hasattr(o, "__iter__"): s = "[" first = True From 37d14865b92a02b406cf195e5591f5f841b4c947 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 16 Nov 2020 08:45:23 -0600 Subject: [PATCH 36/90] Updated for change to polygon() arguments in 126a0728f29282d20f0afdf3aaddbf1a2bb4b4ce --- solid/test/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 32332a5e..0c62648a 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -89,7 +89,7 @@ def test_fillet_2d_add(self): p = polygon(pts) three_points = [euclidify(pts[0:3], Point2)] newp = fillet_2d(three_points, orig_poly=p, fillet_rad=2, remove_material=False) - expected = 'union(){polygon(paths=[[0,1,2,3,4,5]],points=[[0,5],[5,5],[5,0],[10,0],[10,10],[0,10]]);translate(v=[3.0000000000,3.0000000000]){difference(){intersection(){rotate(a=359.9000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=450.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' + expected = 'union(){polygon(points=[[0,5],[5,5],[5,0],[10,0],[10,10],[0,10]]);translate(v=[3.0000000000,3.0000000000]){difference(){intersection(){rotate(a=359.9000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=450.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' actual = scad_render(newp) self.assertEqualNoWhitespace(expected, actual) @@ -97,7 +97,7 @@ def test_fillet_2d_remove(self): pts = list((project_to_2D(p) for p in tri)) poly = polygon(euc_to_arr(pts)) newp = fillet_2d([pts], orig_poly=poly, fillet_rad=2, remove_material=True) - expected = 'difference(){polygon(paths=[[0,1,2]],points=[[0,0],[10,0],[0,10]]);translate(v=[5.1715728753,2.0000000000]){difference(){intersection(){rotate(a=-90.1000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=45.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' + expected = 'difference(){polygon(points=[[0,0],[10,0],[0,10]]);translate(v=[5.1715728753,2.0000000000]){difference(){intersection(){rotate(a=-90.1000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=45.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' actual = scad_render(newp) self.assertEqualNoWhitespace(expected, actual) From 22be6d1eafd9e85ceea92ca441ddf4bcab89e976 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 16 Nov 2020 08:47:16 -0600 Subject: [PATCH 37/90] Tests for accepting imported OpenSCAD calculations as arguments. This resolves #157 as well as #111 --- solid/examples/scad_to_include.scad | 2 ++ solid/test/test_solidpython.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index a67ff9bb..e8a32cef 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -13,4 +13,6 @@ module steps(howmany=3){ } } +function scad_points() = [[0,0], [1,0], [0,1]]; + echo("This text should appear only when called with include(), not use()"); \ No newline at end of file diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index ae3bed60..efa48865 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -17,6 +17,7 @@ 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]]}, }, + {'name': 'polygon', 'class': 'polygon' , 'kwargs': {}, 'expected': '\n\npolygon(points = [[0, 0], [1, 0], [0, 1]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]]}, }, {'name': 'circle', 'class': 'circle' , 'kwargs': {'segments': 12, 'r': 1}, 'expected': '\n\ncircle($fn = 12, r = 1);', 'args': {}, }, {'name': 'circle_diam', 'class': 'circle' , 'kwargs': {'segments': 12, 'd': 1}, 'expected': '\n\ncircle($fn = 12, d = 1);', 'args': {}, }, {'name': 'square', 'class': 'square' , 'kwargs': {'center': False, 'size': 1}, 'expected': '\n\nsquare(center = false, size = 1);', 'args': {}, }, @@ -219,6 +220,16 @@ def test_import_scad(self): # are imported correctly. Not sure how to do this without writing # temp files to those directories. Seems like overkill for the moment + def test_imported_scad_arguments(self): + include_file = self.expand_scad_path("examples/scad_to_include.scad") + mod = import_scad(include_file) + points = mod.scad_points(); + poly = polygon(points); + actual = scad_render(poly); + abs_path = points._get_include_path(include_file) + expected = f'use <{abs_path}>\n\n\npolygon(points = scad_points());' + self.assertEqual(expected, actual) + def test_use_reserved_words(self): scad_str = '''module reserved_word_arg(or=3){\n\tcube(or);\n}\nmodule or(arg=3){\n\tcube(arg);\n}\n''' From 22aee15674792fa55ecb2c590cae4883802dc2e9 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 16 Nov 2020 08:52:00 -0600 Subject: [PATCH 38/90] Bumping to 1.0.3, which contains a fix for #111 & #157 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d79dfc86..550a125e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.0.2" +version = "1.0.3" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From 2802ca72522fb1e007b378e5514526bd1c6d46d1 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 25 Nov 2020 13:52:47 -0600 Subject: [PATCH 39/90] Fix for #158, bringing SP up to date on OpenSCAD's built-in color and alpha handling. Bumping version to 1.0.4. --- README.rst | 30 +++++------------------------- pyproject.toml | 2 +- solid/objects.py | 9 ++++++--- solid/test/test_solidpython.py | 30 ++++++++++++++++++++++++------ solid/utils.py | 2 ++ 5 files changed, 38 insertions(+), 35 deletions(-) diff --git a/README.rst b/README.rst index 1f7e1e14..8383214c 100644 --- a/README.rst +++ b/README.rst @@ -387,38 +387,18 @@ See `solid/examples/path_extrude_example.py `__ for use. -Basic color library +Color & Transparency Settings ------------------- You can change an object's color by using the OpenSCAD -``color([rgba_array])`` function: +``color([rgba_array | rgba hex | SVG color name], alpha)`` function: .. code:: python transparent_blue = color([0,0,1, 0.5])(cube(10)) # Specify with RGB[A] - red_obj = color(Red)(cube(10)) # Or use predefined colors - -These colors are pre-defined in solid.utils: - -+------------+---------+--------------+ -| Red | Green | Blue | -+------------+---------+--------------+ -| Cyan | Magenta | Yellow | -+------------+---------+--------------+ -| Black | White | Transparent | -+------------+---------+--------------+ -| Oak | Pine | Birch | -+------------+---------+--------------+ -| Iron | Steel | Stainless | -+------------+---------+--------------+ -| Aluminum | Brass | BlackPaint | -+------------+---------+--------------+ -| FiberBoard | | | -+------------+---------+--------------+ - -They're a conversion of the materials in the `MCAD OpenSCAD -library `__, as seen [here] -(https://github.com/openscad/MCAD/blob/master/materials.scad). + purple_obj = color('MediumPurple')(cube(10)) # Or use predefined SVG colors + +OpenSCAD natively accepts the W3C's `SVG Color Names `__ Bill Of Materials ----------------- diff --git a/pyproject.toml b/pyproject.toml index 550a125e..52571c02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.0.3" +version = "1.0.4" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" diff --git a/solid/objects.py b/solid/objects.py index 2a4d2019..406a3718 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -376,11 +376,14 @@ class color(OpenSCADObject): not specified. :param c: RGB color + alpha value. - :type c: sequence of 3 or 4 numbers between 0 and 1 + :type c: sequence of 3 or 4 numbers between 0 and 1, OR 3-, 4-, 6-, or 8-digit RGB/A hex code, OR string color name as described at https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Transformations#color + + :param alpha: Alpha value from 0 to 1 + :type alpha: float """ - def __init__(self, c: Vec34) -> None: - super().__init__('color', {'c': c}) + def __init__(self, c: Union[Vec34, str], alpha: float = 1.0) -> None: + super().__init__('color', {'c': c, 'alpha': alpha}) class minkowski(OpenSCADObject): diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index efa48865..aa591669 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -36,7 +36,6 @@ {'name': 'mirror', 'class': 'mirror' , 'kwargs': {}, 'expected': '\n\nmirror(v = [0, 0, 1]);', 'args': {'v': [0, 0, 1]}, }, {'name': 'resize', 'class': 'resize' , 'kwargs': {'newsize': [5, 5, 5], 'auto': [True, True, False]}, 'expected': '\n\nresize(auto = [true, true, false], newsize = [5, 5, 5]);', 'args': {}, }, {'name': 'multmatrix', 'class': 'multmatrix' , 'kwargs': {}, 'expected': '\n\nmultmatrix(m = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]);', 'args': {'m': [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]}, }, - {'name': 'color', 'class': 'color' , 'kwargs': {}, 'expected': '\n\ncolor(c = [1, 0, 0]);', 'args': {'c': [1, 0, 0]}, }, {'name': 'minkowski', 'class': 'minkowski' , 'kwargs': {}, 'expected': '\n\nminkowski();', 'args': {}, }, {'name': 'offset', 'class': 'offset' , 'kwargs': {'r': 1}, 'expected': '\n\noffset(r = 1);', 'args': {}, }, {'name': 'offset_segments', 'class': 'offset' , 'kwargs': {'r': 1, 'segments': 12}, 'expected': '\n\noffset($fn = 12, r = 1);', 'args': {}, }, @@ -114,7 +113,7 @@ def test_infix_intersection(self): def test_parse_scad_callables(self): test_str = """ - module hex (width=10, height=10, + module hex (width=10, height=10, flats= true, center=false){} function righty (angle=90) = 1; function lefty(avar) = 2; @@ -215,11 +214,11 @@ def test_import_scad(self): self.assertTrue(hasattr(examples.scad_to_include, 'steps')) # TODO: we should test that: - # A) scad files in the designated OpenSCAD library directories - # (path-dependent, see: solid.objects._openscad_library_paths()) + # A) scad files in the designated OpenSCAD library directories + # (path-dependent, see: solid.objects._openscad_library_paths()) # are imported correctly. Not sure how to do this without writing # temp files to those directories. Seems like overkill for the moment - + def test_imported_scad_arguments(self): include_file = self.expand_scad_path("examples/scad_to_include.scad") mod = import_scad(include_file) @@ -229,7 +228,7 @@ def test_imported_scad_arguments(self): abs_path = points._get_include_path(include_file) expected = f'use <{abs_path}>\n\n\npolygon(points = scad_points());' self.assertEqual(expected, actual) - + def test_use_reserved_words(self): scad_str = '''module reserved_word_arg(or=3){\n\tcube(or);\n}\nmodule or(arg=3){\n\tcube(arg);\n}\n''' @@ -297,6 +296,25 @@ def test_root(self): actual = scad_render(root(a)) self.assertEqual(expected, actual) + def test_color(self): + all_args = [ + {'c': [1, 0, 0]}, + {'c': [1, 0, 0], 'alpha': 0.5}, + {'c': "#66F"}, + {'c': "Teal", 'alpha': 0.5}, + ] + + expecteds = [ + '\n\ncolor(alpha = 1.0000000000, c = [1, 0, 0]);', + '\n\ncolor(alpha = 0.5000000000, c = [1, 0, 0]);', + '\n\ncolor(alpha = 1.0000000000, c = "#66F");', + '\n\ncolor(alpha = 0.5000000000, c = "Teal");', + ] + for args, expected in zip(all_args, expecteds): + col = color(**args) + actual = scad_render(col) + self.assertEqual(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}' diff --git a/solid/utils.py b/solid/utils.py index 87309e32..40cb9eca 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -52,6 +52,8 @@ # ========== # = Colors = # ========== +# Deprecated, but kept for backwards compatibility . Note that OpenSCAD natively +# accepts SVG Color names, as seen here: https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Transformations#color # From Hans Häggström's materials.scad in MCAD: https://github.com/openscad/MCAD Red = (1, 0, 0) Green = (0, 1, 0) From 2adc937d9dd7e4f9417e0de120bd7ffae9fb6226 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 25 Nov 2020 14:06:36 -0600 Subject: [PATCH 40/90] Removed color section of README; our color() behavior is now identical to OpenSCAD's, like any other SP class --- README.rst | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.rst b/README.rst index 8383214c..f11360d1 100644 --- a/README.rst +++ b/README.rst @@ -27,7 +27,6 @@ SolidPython things: <#directions-up-down-left-right-forward-back-for-arranging-things>`__ - `Arcs <#arcs>`__ - `Extrude Along Path <#extrude_along_path>`__ - - `Basic color library <#basic-color-library>`__ - `Bill Of Materials <#bill-of-materials>`__ - `solid.screw\_thread <#solidscrew_thread>`__ @@ -387,19 +386,6 @@ See `solid/examples/path_extrude_example.py `__ for use. -Color & Transparency Settings -------------------- - -You can change an object's color by using the OpenSCAD -``color([rgba_array | rgba hex | SVG color name], alpha)`` function: - -.. code:: python - - transparent_blue = color([0,0,1, 0.5])(cube(10)) # Specify with RGB[A] - purple_obj = color('MediumPurple')(cube(10)) # Or use predefined SVG colors - -OpenSCAD natively accepts the W3C's `SVG Color Names `__ - Bill Of Materials ----------------- From 98e741e6ba92a7bbd55ad01970c75e273237742e Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 1 Feb 2021 18:06:10 -0600 Subject: [PATCH 41/90] Added a `traits` dictionary on OpenSCADObject. This is used to collect information for solid.utils.bill_of_materials()`, but could be used for other metadata storage. --- solid/solidpython.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 07cb3549..65244a25 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -21,7 +21,7 @@ from typing import Set, Sequence, List, Callable, Optional, Union, Iterable from types import ModuleType -from typing import Callable, Iterable, List, Optional, Sequence, Set, Union +from typing import Callable, Iterable, List, Optional, Sequence, Set, Union, Dict import pkg_resources import regex as re @@ -52,6 +52,13 @@ def __init__(self, name: str, params: dict): self.is_hole = False self.has_hole_children = False self.is_part_root = False + self.traits: Dict[str, Dict[str, float]] = {} + + def add_trait(self, trait_name:str, trait_data:Dict[str, float]): + self.traits[trait_name] = trait_data + + def get_trait(self, trait_name:str) -> Optional[Dict[str, float]]: + return self.traits.get(trait_name) def set_hole(self, is_hole: bool = True) -> "OpenSCADObject": self.is_hole = is_hole From c97197def83b2497e71a46f72cff74c600ead776 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 1 Feb 2021 18:07:24 -0600 Subject: [PATCH 42/90] Fixes #162; We had kept track of Bill of Materials (BOM) metadata in a global variable in the solid.utils module. Instead, it's now stored on objects themselves. --- pyproject.toml | 15 +++++++++++-- solid/examples/bom_scad.py | 11 +++++----- solid/utils.py | 43 ++++++++++++++++++++++++++++---------- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52571c02..e528ee1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.0.4" +version = "1.0.5" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" @@ -42,7 +42,18 @@ regex = "^2019.4" [tool.poetry.dev-dependencies] tox = "^tox 3.11" + [build-system] -requires = ["poetry>=0.12"] +requires = [ + "poetry>=0.12", + # See https://github.com/pypa/setuptools/issues/2353#issuecomment-683781498 + # for the rest of these requirements, + # -ETJ 31 December 2020 + "setuptools>=30.3.0,<50", + "wheel", + "pytest-runner", + "setuptools_scm>=3.3.1", +] + build-backend = "poetry.masonry.api" diff --git a/solid/examples/bom_scad.py b/solid/examples/bom_scad.py index a059a66f..7611835c 100755 --- a/solid/examples/bom_scad.py +++ b/solid/examples/bom_scad.py @@ -93,15 +93,16 @@ def doohickey(c): def assembly(): + nut = m3_nut() return union()( doohickey(c='blue'), translate((-10, 0, doohickey_h / 2))(m3_12()), translate((0, 0, doohickey_h / 2))(m3_16()), translate((10, 0, doohickey_h / 2))(m3_12()), # Nuts - translate((-10, 0, -nut_height - doohickey_h / 2))(m3_nut()), - translate((0, 0, -nut_height - doohickey_h / 2))(m3_nut()), - translate((10, 0, -nut_height - doohickey_h / 2))(m3_nut()), + translate((-10, 0, -nut_height - doohickey_h / 2))(nut), + translate((0, 0, -nut_height - doohickey_h / 2))(nut), + translate((10, 0, -nut_height - doohickey_h / 2))(nut), ) @@ -109,12 +110,12 @@ def assembly(): out_dir = sys.argv[1] if len(sys.argv) > 1 else None a = assembly() - bom = bill_of_materials() + bom = bill_of_materials(a) file_out = scad_render_to_file(a, out_dir=out_dir) print(f"{__file__}: SCAD file written to: \n{file_out}") print(bom) print("Or, Spreadsheet-ready TSV:\n\n") - bom = bill_of_materials(csv=True) + bom = bill_of_materials(a, csv=True) print(bom) diff --git a/solid/utils.py b/solid/utils.py index 40cb9eca..3bbdb10f 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -483,7 +483,7 @@ def section_cut_xz(body: OpenSCADObject, y_cut_point:float=0) -> OpenSCADObject: # ===================== # Any part defined in a method can be automatically counted using the # `@bom_part()` decorator. After all parts have been created, call -# `bill_of_materials()` +# `bill_of_materials()` # to generate a report. See `examples/bom_scad.py` for usage # # Additional columns can be added (such as leftover material or URL to part) @@ -493,7 +493,6 @@ def section_cut_xz(body: OpenSCADObject, y_cut_point:float=0) -> OpenSCADObject: # populate the new columns in order of their addition via bom_headers, or # keyworded arguments can be used in any order. -g_parts_dict = {} g_bom_headers: List[str] = [] def set_bom_headers(*args): @@ -504,31 +503,53 @@ def bom_part(description: str='', per_unit_price:float=None, currency: str='US$' def wrap(f): name = description if description else f.__name__ - elements = {} - elements.update({'Count':0, 'currency':currency, 'Unit Price':per_unit_price}) + elements = {'name': name, 'Count':0, 'currency':currency, 'Unit Price':per_unit_price} # This update also adds empty key value pairs to prevent key exceptions. elements.update(dict(zip_longest(g_bom_headers, args, fillvalue=''))) elements.update(kwargs) - g_parts_dict[name] = elements - def wrapped_f(*wargs, **wkwargs): - name = description if description else f.__name__ - g_parts_dict[name]['Count'] += 1 - return f(*wargs, **wkwargs) + scad_obj = f(*wargs, **wkwargs) + scad_obj.add_trait('BOM', elements) + return scad_obj return wrapped_f return wrap -def bill_of_materials(csv:bool=False) -> str: +def bill_of_materials(root_obj:OpenSCADObject, csv:bool=False) -> str: + traits_dicts = _traits_bom_dicts(root_obj) + # Build a single dictionary from the ones stored on each child object + # (This is an adaptation of an earlier version, and probably not the most + # direct way to accomplish this) + all_bom_traits = {} + for traits_dict in traits_dicts: + name = traits_dict['name'] + if name in all_bom_traits: + all_bom_traits[name]['Count'] += 1 + else: + all_bom_traits[name] = traits_dict + all_bom_traits[name]['Count'] = 1 + bom = _make_bom(all_bom_traits, csv) + return bom + +def _traits_bom_dicts(root_obj:OpenSCADObject) -> List[Dict[str, float]]: + all_child_traits = [_traits_bom_dicts(c) for c in root_obj.children] + child_traits = [item for subl in all_child_traits for item in subl if item] + bom_trait = root_obj.get_trait('BOM') + if bom_trait: + child_traits.append(bom_trait) + return child_traits + +def _make_bom(bom_parts_dict: Dict[str, float], csv:bool=False, ) -> str: field_names = ["Description", "Count", "Unit Price", "Total Price"] field_names += g_bom_headers rows = [] all_costs: Dict[str, float] = {} - for desc, elements in g_parts_dict.items(): + for desc, elements in bom_parts_dict.items(): + row = [] count = elements['Count'] currency = elements['currency'] price = elements['Unit Price'] From 4a4872fdbb69142d5d5de9d08d3ca528715cdf49 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 14 Feb 2021 09:15:31 -0600 Subject: [PATCH 43/90] Fix for #163. All OpenSCAD arguments are effectively optional, while all arguments without default values are required in Python. Resolved to match OpenSCAD's semantics --- solid/solidpython.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 65244a25..c8b85a33 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -653,10 +653,10 @@ def parse_scad_callables(scad_code_str: str) -> List[dict]: arg_matches = re.finditer(args_re, all_args) for am in arg_matches: arg_name = am.group('arg_name') - if am.group('default_val'): - kwargs.append(arg_name) - else: - args.append(arg_name) + # NOTE: OpenSCAD's arguments to all functions are effectively + # optional, in contrast to Python in which all args without + # default values are required. + kwargs.append(arg_name) callables.append({'name': callable_name, 'args': args, 'kwargs': kwargs}) From fa260864ce82bc6a4b8c72927ae307294b9dc566 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 14 Feb 2021 09:40:28 -0600 Subject: [PATCH 44/90] Added tests for #163 --- solid/examples/scad_to_include.scad | 7 +++++++ solid/test/test_solidpython.py | 14 ++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index e8a32cef..b2c04ba7 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -15,4 +15,11 @@ module steps(howmany=3){ function scad_points() = [[0,0], [1,0], [0,1]]; +// In Python, calling this function without an argument would be an error. +// Leave this here to confirm that this works in OpenSCAD. +function optional_nondefault_arg(arg1){ + s = arg1 ? arg1 : 1; + cube([s,s,s]); +} + echo("This text should appear only when called with include(), not use()"); \ No newline at end of file diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index aa591669..7388f021 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -143,17 +143,17 @@ def test_parse_scad_callables(self): module var_with_arithmetic(var_with_arithmetic = 8 * 9 - 1 + 89 / 15){} module var_with_parentheses(var_with_parentheses = 8 * ((9 - 1) + 89) / 15){} module var_with_functions(var_with_functions = abs(min(chamferHeight2, 0)) */-+ 1){} - module var_with_conditionnal_assignment(var_with_conditionnal_assignment = mytest ? 45 : yop){} + module var_with_conditional_assignment(var_with_conditional_assignment = mytest ? 45 : yop){} """ expected = [ {'name': 'hex', 'args': [], 'kwargs': ['width', 'height', 'flats', 'center']}, {'name': 'righty', 'args': [], 'kwargs': ['angle']}, - {'name': 'lefty', 'args': ['avar'], 'kwargs': []}, + {'name': 'lefty', 'args': [], 'kwargs': ['avar']}, {'name': 'more', 'args': [], 'kwargs': ['a']}, {'name': 'pyramid', 'args': [], 'kwargs': ['side', 'height', 'square', 'centerHorizontal', 'centerVertical']}, {'name': 'no_comments', 'args': [], 'kwargs': ['arg', 'other_arg', 'last_arg']}, {'name': 'float_arg', 'args': [], 'kwargs': ['arg']}, - {'name': 'arg_var', 'args': ['var5'], 'kwargs': []}, + {'name': 'arg_var', 'args': [], 'kwargs': ['var5']}, {'name': 'kwarg_var', 'args': [], 'kwargs': ['var2']}, {'name': 'var_true', 'args': [], 'kwargs': ['var_true']}, {'name': 'var_false', 'args': [], 'kwargs': ['var_false']}, @@ -172,7 +172,7 @@ def test_parse_scad_callables(self): {'name': 'var_with_arithmetic', 'args': [], 'kwargs': ['var_with_arithmetic']}, {'name': 'var_with_parentheses', 'args': [], 'kwargs': ['var_with_parentheses']}, {'name': 'var_with_functions', 'args': [], 'kwargs': ['var_with_functions']}, - {'name': 'var_with_conditionnal_assignment', 'args': [], 'kwargs': ['var_with_conditionnal_assignment']} + {'name': 'var_with_conditional_assignment', 'args': [], 'kwargs': ['var_with_conditional_assignment']} ] from solid.solidpython import parse_scad_callables @@ -205,6 +205,12 @@ def test_import_scad(self): expected = f"{header}\nuse <{abs_path}>\n\n\nsteps(howmany = 3);" self.assertEqual(expected, actual) + # Confirm that we can leave out even non-default arguments in OpenSCAD + a = mod.optional_nondefault_arg(); + actual = scad_render(a) + expected = f'use <{abs_path}>\n\n\noptional_nondefault_arg();' + self.assertEqual(expected, actual); + # Make sure we throw ValueError on nonexistent imports self.assertRaises(ValueError, import_scad, 'path/doesnt/exist.scad') From a045f82279bd90a1005dfae0e2be2407927d8de6 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sun, 7 Mar 2021 20:00:08 -0600 Subject: [PATCH 45/90] Inspired by mickael.bosch@posteo.net, Let `solid.utils.extrude_along_path()` accept a list of Point2 for scale_factors, allowing independent x & y scaling for each point along the extrusion path. Added tests & expanded example --- solid/examples/path_extrude_example.py | 88 ++++++++++++++++++++------ solid/test/test_utils.py | 1 + solid/utils.py | 28 ++++---- 3 files changed, 83 insertions(+), 34 deletions(-) diff --git a/solid/examples/path_extrude_example.py b/solid/examples/path_extrude_example.py index 12734607..4e0d9c8a 100755 --- a/solid/examples/path_extrude_example.py +++ b/solid/examples/path_extrude_example.py @@ -1,11 +1,16 @@ #! /usr/bin/env python3 +from solid.solidpython import OpenSCADObject import sys -from math import cos, radians, sin +from math import cos, radians, sin, pi, tau +from pathlib import Path -from euclid3 import Point3 +from euclid3 import Point2, Point3 -from solid import scad_render_to_file -from solid.utils import extrude_along_path +from solid import scad_render_to_file, text, translate +from solid.utils import extrude_along_path, right + + +from typing import Set, Sequence, List, Callable, Optional, Union, Iterable SEGMENTS = 48 @@ -13,10 +18,13 @@ def sinusoidal_ring(rad=25, segments=SEGMENTS): outline = [] for i in range(segments): - angle = i * 360 / segments - x = rad * cos(radians(angle)) - y = rad * sin(radians(angle)) - z = 2 * sin(radians(angle * 6)) + angle = radians(i * 360 / segments) + scaled_rad = (1 + 0.18*cos(angle*5)) * rad + x = scaled_rad * cos(angle) + y = scaled_rad * sin(angle) + z = 0 + # Or stir it up and add an oscillation in z as well + # z = 3 * sin(angle * 6) outline.append(Point3(x, y, z)) return outline @@ -29,28 +37,68 @@ def star(num_points=5, outer_rad=15, dip_factor=0.5): star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0)) return star_pts +def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]: + angles = [tau/num_points * i for i in range(num_points)] + points = list([Point2(rad*cos(a), rad*sin(a)) for a in angles]) + return points def extrude_example(): # Note the incorrect triangulation at the two ends of the path. This # is because star isn't convex, and the triangulation algorithm for # the two end caps only works for convex shapes. + path_rad = 50 shape = star(num_points=5) - path = sinusoidal_ring(rad=50) + path = sinusoidal_ring(rad=path_rad, segments=240) + + # # If scale_factors aren't included, they'll default to + # # no scaling at each step along path. Here, let's + # # make the shape twice as big at beginning and end of the path + # scales = [1] * len(path) + # n = len(path) + # scales = [1 + 0.5*sin(i*6*pi/n) for i in range(n)] + # scales[0] = 2 + # scales[-1] = 2 + + extruded = extrude_along_path( shape_pts=shape, path_pts=path) + # Label + extruded += translate([-path_rad/2, 2*path_rad])(text('Basic Extrude')) + return extruded + +def extrude_example_xy_scaling() -> OpenSCADObject: + num_points = SEGMENTS + path_rad = 50 + circle = circle_points(15) + path = circle_points(rad = path_rad) + + # angle: from 0 to 6*Pi + angles = list((i/(num_points - 1)*tau*3 for i in range(len(path)))) + # If scale_factors aren't included, they'll default to - # no scaling at each step along path. Here, let's - # make the shape twice as big at beginning and end of the path - scales = [1] * len(path) - scales[0] = 2 - scales[-1] = 2 + # no scaling at each step along path. + no_scale_obj = translate([-path_rad / 2, 2 * path_rad])(text('No Scale')) + no_scale_obj += extrude_along_path(circle, path) - extruded = extrude_along_path(shape_pts=shape, path_pts=path, scale_factors=scales) + # With a 1-D scale factor, an extrusion grows and shrinks uniformly + x_scales = [(1 + cos(a)/2) for a in angles] + x_obj = translate([-path_rad / 2, 2 * path_rad])(text('1D Scale')) + x_obj += extrude_along_path(circle, path, scale_factors=x_scales) - return extruded + # With a 2D scale factor, a shape's X & Y dimensions can scale + # independently, leading to more interesting shapes + # X & Y scales vary between 0.5 & 1.5 + xy_scales = [Point2( 1 + cos(a)/2, 1 + sin(a)/2) for a in angles] + xy_obj = translate([-path_rad / 2, 2 * path_rad])( text('2D Scale')) + xy_obj += extrude_along_path(circle, path, scale_factors=xy_scales) + + obj = no_scale_obj + right(3*path_rad)(x_obj) + right(6 * path_rad)(xy_obj) + return obj +if __name__ == "__main__": + out_dir = sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent -if __name__ == '__main__': - out_dir = sys.argv[1] if len(sys.argv) > 1 else None - a = extrude_example() - file_out = scad_render_to_file(a, out_dir=out_dir, include_orig_code=True) + basic_extrude = extrude_example() + scaled_extrusions = extrude_example_xy_scaling() + a = basic_extrude + translate([0,-250])(scaled_extrusions) + file_out = scad_render_to_file(a, out_dir=out_dir, include_orig_code=True) print(f"{__file__}: SCAD file written to: \n{file_out}") diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 0c62648a..9d9c12e5 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -29,6 +29,7 @@ ('arc_inverted', arc_inverted, [10, 0, 90, 24], '\n\ndifference() {\n\tintersection() {\n\t\trotate(a = 0) {\n\t\t\ttranslate(v = [-990, 0, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t\trotate(a = 90) {\n\t\t\ttranslate(v = [-990, -1000, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t}\n\tcircle($fn = 24, r = 10);\n}'), ('transform_to_point_scad', transform_to_point, [cube(2), [2, 2, 2], [3, 3, 1]], '\n\nmultmatrix(m = [[0.7071067812, -0.1622214211, -0.6882472016, 2], [-0.7071067812, -0.1622214211, -0.6882472016, 2], [0.0000000000, 0.9733285268, -0.2294157339, 2], [0, 0, 0, 1.0000000000]]) {\n\tcube(size = 2);\n}'), ('extrude_along_path', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000]]);'), + ('extrude_along_path_xy_scale', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]], [Point2(0.5, 1.5), Point2(1.5, 0.5)]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [5.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 15.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [15.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 5.0000000000]]);'), ('extrude_along_path_vertical', extrude_along_path, [tri, [[0, 0, 0], [0, 0, 20]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [-10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 10.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [-10.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, 10.0000000000, 20.0000000000]]);'), ] diff --git a/solid/utils.py b/solid/utils.py index 3bbdb10f..90ef59e9 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -1120,7 +1120,7 @@ def path_2d_polygon(points:Sequence[Point23], width:float=1, closed:bool=False) # ========================== def extrude_along_path( shape_pts:Points, path_pts:Points, - scale_factors:Sequence[float]=None) -> OpenSCADObject: + scale_factors:Sequence[Union[Vector2, float]]=None) -> OpenSCADObject: # Extrude the convex curve defined by shape_pts along path_pts. # -- For predictable results, shape_pts must be planar, convex, and lie # in the XY plane centered around the origin. @@ -1132,9 +1132,6 @@ def extrude_along_path( shape_pts:Points, polyhedron_pts:Points= [] facet_indices:List[Tuple[int, int, int]] = [] - if not scale_factors: - scale_factors = [1.0] * len(path_pts) - # Make sure we've got Euclid Point3's for all elements shape_pts = euclidify(shape_pts, Point3) path_pts = euclidify(path_pts, Point3) @@ -1143,7 +1140,6 @@ def extrude_along_path( shape_pts:Points, for which_loop in range(len(path_pts)): path_pt = path_pts[which_loop] - scale = float(scale_factors[which_loop]) # calculate the tangent to the curve at this point if which_loop > 0 and which_loop < len(path_pts) - 1: @@ -1159,17 +1155,21 @@ def extrude_along_path( shape_pts:Points, tangent = path_pt - path_pts[which_loop - 1] # Scale points - this_loop:Point3 = [] - if scale != 1.0: - this_loop = [(scale * sh) for sh in shape_pts] - # Convert this_loop back to points; scaling changes them to Vectors - this_loop = [Point3(v.x, v.y, v.z) for v in this_loop] - else: - this_loop = shape_pts[:] # type: ignore + this_loop = shape_pts[:] # type: ignore + scale_x, scale_y = [1, 1] + if scale_factors: + scale = scale_factors[which_loop] + if isinstance(scale, (int, float)): + scale_x, scale_y = scale, scale + elif isinstance(scale, Vector2): + scale_x, scale_y = scale.x, scale.y + else: + raise ValueError(f'Unable to scale shape_pts with scale value: {scale}') + this_loop = [Point3(v.x * scale_x, v.y * scale_y, v.z) for v in this_loop] # Rotate & translate - this_loop = transform_to_point(this_loop, dest_point=path_pt, - dest_normal=tangent, src_up=src_up) + this_loop = transform_to_point(this_loop, dest_point=path_pt, + dest_normal=tangent, src_up=src_up) # Add the transformed points to our final list polyhedron_pts += this_loop From b388e4403eef273bc7cab7e9b211ab207779caad Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 14:10:34 -0600 Subject: [PATCH 46/90] Improved end cap algorithm on solid.utils.extrude_along_path(). It can now accept somewhat concave polygon (e.g., stars) profiles and still return a valid non-intersecting triangulation of a polygon. --- solid/examples/path_extrude_example.py | 13 ----- solid/utils.py | 71 ++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/solid/examples/path_extrude_example.py b/solid/examples/path_extrude_example.py index 4e0d9c8a..d21e46fe 100755 --- a/solid/examples/path_extrude_example.py +++ b/solid/examples/path_extrude_example.py @@ -43,22 +43,10 @@ def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]: return points def extrude_example(): - # Note the incorrect triangulation at the two ends of the path. This - # is because star isn't convex, and the triangulation algorithm for - # the two end caps only works for convex shapes. path_rad = 50 shape = star(num_points=5) path = sinusoidal_ring(rad=path_rad, segments=240) - # # If scale_factors aren't included, they'll default to - # # no scaling at each step along path. Here, let's - # # make the shape twice as big at beginning and end of the path - # scales = [1] * len(path) - # n = len(path) - # scales = [1 + 0.5*sin(i*6*pi/n) for i in range(n)] - # scales[0] = 2 - # scales[-1] = 2 - extruded = extrude_along_path( shape_pts=shape, path_pts=path) # Label extruded += translate([-path_rad/2, 2*path_rad])(text('Basic Extrude')) @@ -73,7 +61,6 @@ def extrude_example_xy_scaling() -> OpenSCADObject: # angle: from 0 to 6*Pi angles = list((i/(num_points - 1)*tau*3 for i in range(len(path)))) - # If scale_factors aren't included, they'll default to # no scaling at each step along path. no_scale_obj = translate([-path_rad / 2, 2 * path_rad])(text('No Scale')) diff --git a/solid/utils.py b/solid/utils.py index 90ef59e9..6f3bd102 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -20,9 +20,12 @@ from typing import Any, Union, Tuple, Sequence, List, Optional, Callable, Dict, cast Point23 = Union[Point2, Point3] Vector23 = Union[Vector2, Vector3] +PointVec23 = Union[Point2, Point3, Vector2, Vector3] Line23 = Union[Line2, Line3] LineSegment23 = Union[LineSegment2, LineSegment3] +FacetIndices = Tuple[int, int, int] + Tuple2 = Tuple[float, float] Tuple3 = Tuple[float, float, float] EucOrTuple = Union[Point3, @@ -107,7 +110,6 @@ def grid_plane(grid_unit:int=12, count:int=10, line_weight:float=0.1, plane:str= return t - def distribute_in_grid(objects:Sequence[OpenSCADObject], max_bounding_box:Tuple[float,float], rows_and_cols: Tuple[int,int]=None) -> OpenSCADObject: @@ -814,6 +816,27 @@ def scad_matrix(euclid_matrix4): [a.m, a.n, a.o, a.p] ] +def centroid(points:Sequence[PointVec23]) -> PointVec23: + if not points: + raise ValueError(f"centroid(): argument `points` is empty") + first = points[0] + is_3d = isinstance(first, (Vector3, Point3)) + if is_3d: + total = Vector3(0,0,0) + else: + total = Vector2(0, 0) + + for p in points: + total += p + total /= len(points) + + if isinstance(first, Point2): + return Point2(*total) + elif isinstance(first, Point3): + return Point3(*total) + else: + return total + # ============== # = Transforms = # ============== @@ -1184,19 +1207,45 @@ def extrude_along_path( shape_pts:Points, facet_indices.append( (segment_start, segment_end, segment_end + shape_pt_count) ) facet_indices.append( (segment_start, segment_end + shape_pt_count, segment_start + shape_pt_count) ) - # Cap the start of the polyhedron - for i in range(1, shape_pt_count - 1): - facet_indices.append((0, i, i + 1)) - - # And the end (could be rolled into the earlier loop) - # FIXME: concave cross-sections will cause this end-capping algorithm - # to fail - end_cap_base = len(polyhedron_pts) - shape_pt_count - for i in range(end_cap_base + 1, len(polyhedron_pts) - 1): - facet_indices.append( (end_cap_base, i + 1, i) ) + # endcap at start of extrusion + start_cap_index = len(polyhedron_pts) + start_loop_pts = polyhedron_pts[:shape_pt_count] + start_loop_indices = list(range(shape_pt_count)) + start_centroid, start_facet_indices = end_cap(start_cap_index, start_loop_pts, start_loop_indices) + polyhedron_pts.append(start_centroid) + facet_indices += start_facet_indices + + # endcap at end of extrusion + end_cap_index = len(polyhedron_pts) + last_loop_start_index = len(polyhedron_pts) - shape_pt_count - 1 + end_loop_pts = polyhedron_pts[last_loop_start_index:-1] + end_loop_indices = list(range(last_loop_start_index, len(polyhedron_pts) - 1)) + end_centroid, end_facet_indices = end_cap(end_cap_index, end_loop_pts, end_loop_indices) + polyhedron_pts.append(end_centroid) + facet_indices += end_facet_indices return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore +def end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]: + # Assume points are a basically planar, basically convex polygon with polyhedron + # indices `vertex_indices`. + # Return a new point that is the centroid of the polygon and a list of + # vertex triangle indices that covers the whole polygon. + # (We can actually accept relatively non-planar and non-convex polygons, + # but not anything pathological. Stars are fine, internal pockets would + # cause incorrect faceting) + + # NOTE: In order to deal with moderately-concave polygons, we add a point + # to the center of the end cap. This will have a new index that we require + # as an argument. + + new_point = centroid(points) + new_facets = [] + second_indices = vertex_indices[1:] + [vertex_indices[0]] + new_facets = [(new_point_index, a, b) for a, b in zip(vertex_indices, second_indices)] + + return (new_point, new_facets) + def frange(*args): """ # {{{ http://code.activestate.com/recipes/577068/ (r1) From b050614af2bb841a9adbf35d4d18795785f5c2b7 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 16:12:32 -0600 Subject: [PATCH 47/90] -- Added `connect_ends` argument to solid.utils.extrude_along_path(), which will create closed continuous shapes if specified -- Reworked endcap algorithm in solid.utils.extrude_along_path() so it works with *some* concave shapes -- Updated with examples of each feature. --- solid/examples/path_extrude_example.py | 79 ++++++++------ solid/utils.py | 138 ++++++++++++++----------- 2 files changed, 127 insertions(+), 90 deletions(-) diff --git a/solid/examples/path_extrude_example.py b/solid/examples/path_extrude_example.py index d21e46fe..7578895e 100755 --- a/solid/examples/path_extrude_example.py +++ b/solid/examples/path_extrude_example.py @@ -14,35 +14,7 @@ SEGMENTS = 48 - -def sinusoidal_ring(rad=25, segments=SEGMENTS): - outline = [] - for i in range(segments): - angle = radians(i * 360 / segments) - scaled_rad = (1 + 0.18*cos(angle*5)) * rad - x = scaled_rad * cos(angle) - y = scaled_rad * sin(angle) - z = 0 - # Or stir it up and add an oscillation in z as well - # z = 3 * sin(angle * 6) - outline.append(Point3(x, y, z)) - return outline - - -def star(num_points=5, outer_rad=15, dip_factor=0.5): - star_pts = [] - for i in range(2 * num_points): - rad = outer_rad - i % 2 * dip_factor * outer_rad - angle = radians(360 / (2 * num_points) * i) - star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0)) - return star_pts - -def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]: - angles = [tau/num_points * i for i in range(num_points)] - points = list([Point2(rad*cos(a), rad*sin(a)) for a in angles]) - return points - -def extrude_example(): +def basic_extrude_example(): path_rad = 50 shape = star(num_points=5) path = sinusoidal_ring(rad=path_rad, segments=240) @@ -81,11 +53,56 @@ def extrude_example_xy_scaling() -> OpenSCADObject: obj = no_scale_obj + right(3*path_rad)(x_obj) + right(6 * path_rad)(xy_obj) return obj +def extrude_example_capped_ends() -> OpenSCADObject: + num_points = SEGMENTS/2 + path_rad = 50 + circle = star(6) + path = circle_points(rad = path_rad) + + # If `connect_ends` is False or unspecified, ends will be capped. + # Endcaps will be correct for most convex or mildly concave (e.g. stars) cross sections + capped_obj = translate([-path_rad / 2, 2 * path_rad])(text('Capped Ends')) + capped_obj += extrude_along_path(circle, path, connect_ends=False) + + # If `connect_ends` is specified, create a continuous manifold object + connected_obj = translate([-path_rad / 2, 2 * path_rad])(text('Connected Ends')) + connected_obj += extrude_along_path(circle, path, connect_ends=True) + + return capped_obj + right(3*path_rad)(connected_obj) + +def sinusoidal_ring(rad=25, segments=SEGMENTS): + outline = [] + for i in range(segments): + angle = radians(i * 360 / segments) + scaled_rad = (1 + 0.18*cos(angle*5)) * rad + x = scaled_rad * cos(angle) + y = scaled_rad * sin(angle) + z = 0 + # Or stir it up and add an oscillation in z as well + # z = 3 * sin(angle * 6) + outline.append(Point3(x, y, z)) + return outline + +def star(num_points=5, outer_rad=15, dip_factor=0.5): + star_pts = [] + for i in range(2 * num_points): + rad = outer_rad - i % 2 * dip_factor * outer_rad + angle = radians(360 / (2 * num_points) * i) + star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0)) + return star_pts + +def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]: + angles = [tau/num_points * i for i in range(num_points)] + points = list([Point2(rad*cos(a), rad*sin(a)) for a in angles]) + return points + if __name__ == "__main__": out_dir = sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent - basic_extrude = extrude_example() + basic_extrude = basic_extrude_example() scaled_extrusions = extrude_example_xy_scaling() - a = basic_extrude + translate([0,-250])(scaled_extrusions) + capped_extrusions = extrude_example_capped_ends() + a = basic_extrude + translate([0,-250])(scaled_extrusions) + translate([0, -500])(capped_extrusions) + file_out = scad_render_to_file(a, out_dir=out_dir, include_orig_code=True) print(f"{__file__}: SCAD file written to: \n{file_out}") diff --git a/solid/utils.py b/solid/utils.py index 6f3bd102..c7e1eec8 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -1143,7 +1143,8 @@ def path_2d_polygon(points:Sequence[Point23], width:float=1, closed:bool=False) # ========================== def extrude_along_path( shape_pts:Points, path_pts:Points, - scale_factors:Sequence[Union[Vector2, float]]=None) -> OpenSCADObject: + scale_factors:Sequence[Union[Vector2, float]]=None, + connect_ends = False) -> OpenSCADObject: # Extrude the convex curve defined by shape_pts along path_pts. # -- For predictable results, shape_pts must be planar, convex, and lie # in the XY plane centered around the origin. @@ -1161,72 +1162,91 @@ def extrude_along_path( shape_pts:Points, src_up = Vector3(*UP_VEC) + shape_pt_count = len(shape_pts) + + tangent_path_points: List[Point3] = [] + if connect_ends: + tangent_path_points = [path_pts[-1]] + path_pts + [path_pts[0]] + else: + first = Point3(*(path_pts[0] - (path_pts[1] - path_pts[0]))) + last = Point3(*(path_pts[-1] - (path_pts[-2] - path_pts[-1]))) + tangent_path_points = [first] + path_pts + [last] + tangents = [tangent_path_points[i+2] - tangent_path_points[i] for i in range(len(path_pts))] + for which_loop in range(len(path_pts)): path_pt = path_pts[which_loop] + tangent = tangents[which_loop] + scale_factor = scale_factors[which_loop] if scale_factors else 1 + this_loop = shape_pts[:] + this_loop = _scale_loop(this_loop, scale_factor) + this_loop = transform_to_point(this_loop, dest_point=path_pt, dest_normal=tangent, src_up=src_up) + loop_start_index = which_loop * shape_pt_count + + if (which_loop < len(path_pts) - 1): + loop_facets = _loop_facet_indices(loop_start_index, shape_pt_count) + facet_indices += loop_facets + + # Add the transformed points & facets to our final list + polyhedron_pts += this_loop - # calculate the tangent to the curve at this point - if which_loop > 0 and which_loop < len(path_pts) - 1: - prev_pt = path_pts[which_loop - 1] - next_pt = path_pts[which_loop + 1] - - v_prev = path_pt - prev_pt - v_next = next_pt - path_pt - tangent = v_prev + v_next - elif which_loop == 0: - tangent = path_pts[which_loop + 1] - path_pt - elif which_loop == len(path_pts) - 1: - tangent = path_pt - path_pts[which_loop - 1] - - # Scale points - this_loop = shape_pts[:] # type: ignore - scale_x, scale_y = [1, 1] - if scale_factors: - scale = scale_factors[which_loop] - if isinstance(scale, (int, float)): - scale_x, scale_y = scale, scale - elif isinstance(scale, Vector2): - scale_x, scale_y = scale.x, scale.y - else: - raise ValueError(f'Unable to scale shape_pts with scale value: {scale}') - this_loop = [Point3(v.x * scale_x, v.y * scale_y, v.z) for v in this_loop] + if connect_ends: + next_loop_start_index = len(polyhedron_pts) - shape_pt_count + loop_facets = _loop_facet_indices(0, shape_pt_count, next_loop_start_index) + facet_indices += loop_facets - # Rotate & translate - this_loop = transform_to_point(this_loop, dest_point=path_pt, - dest_normal=tangent, src_up=src_up) + else: + # endcaps at start & end of extrusion + # NOTE: this block adds points & indices to the polyhedron, so it's + # very sensitive to the order this is happening in + start_cap_index = len(polyhedron_pts) + end_cap_index = start_cap_index + 1 + last_loop_start_index = len(polyhedron_pts) - shape_pt_count - # Add the transformed points to our final list - polyhedron_pts += this_loop - # And calculate the facet indices - shape_pt_count = len(shape_pts) - segment_start = which_loop * shape_pt_count - segment_end = segment_start + shape_pt_count - 1 - if which_loop < len(path_pts) - 1: - for i in range(segment_start, segment_end): - facet_indices.append( (i, i + shape_pt_count, i + 1) ) - facet_indices.append( (i + 1, i + shape_pt_count, i + shape_pt_count + 1) ) - facet_indices.append( (segment_start, segment_end, segment_end + shape_pt_count) ) - facet_indices.append( (segment_start, segment_end + shape_pt_count, segment_start + shape_pt_count) ) - - # endcap at start of extrusion - start_cap_index = len(polyhedron_pts) - start_loop_pts = polyhedron_pts[:shape_pt_count] - start_loop_indices = list(range(shape_pt_count)) - start_centroid, start_facet_indices = end_cap(start_cap_index, start_loop_pts, start_loop_indices) - polyhedron_pts.append(start_centroid) - facet_indices += start_facet_indices - - # endcap at end of extrusion - end_cap_index = len(polyhedron_pts) - last_loop_start_index = len(polyhedron_pts) - shape_pt_count - 1 - end_loop_pts = polyhedron_pts[last_loop_start_index:-1] - end_loop_indices = list(range(last_loop_start_index, len(polyhedron_pts) - 1)) - end_centroid, end_facet_indices = end_cap(end_cap_index, end_loop_pts, end_loop_indices) - polyhedron_pts.append(end_centroid) - facet_indices += end_facet_indices + start_loop_pts = polyhedron_pts[:shape_pt_count] + end_loop_pts = polyhedron_pts[last_loop_start_index:] + + start_loop_indices = list(range(0, shape_pt_count)) + end_loop_indices = list(range(last_loop_start_index, last_loop_start_index + shape_pt_count)) + + start_centroid, start_facet_indices = _end_cap(start_cap_index, start_loop_pts, start_loop_indices) + end_centroid, end_facet_indices = _end_cap(end_cap_index, end_loop_pts, end_loop_indices) + polyhedron_pts += [start_centroid, end_centroid] + facet_indices += start_facet_indices + facet_indices += end_facet_indices return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore -def end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]: +def _loop_facet_indices(loop_start_index:int, loop_pt_count:int, next_loop_start_index=None) -> List[FacetIndices]: + facet_indices: List[FacetIndices] = [] + # nlsi == next_loop_start_index + if next_loop_start_index == None: + next_loop_start_index = loop_start_index + loop_pt_count + loop_indices = list(range(loop_start_index, loop_pt_count + loop_start_index)) + [loop_start_index] + next_loop_indices = list(range(next_loop_start_index, loop_pt_count + next_loop_start_index )) + [next_loop_start_index] + + for i, (a, b) in enumerate(zip(loop_indices[:-1], loop_indices[1:])): + # c--d + # |\ | + # | \| + # a--b + c, d = next_loop_indices[i: i+2] + facet_indices.append((a,c,b)) + facet_indices.append((b,c,d)) + return facet_indices + +def _scale_loop(points:Sequence[Point3], scale_factor:Union[float, Point2]=None) -> List[Point3]: + scale_x, scale_y = [1, 1] + if scale_factor: + if isinstance(scale_factor, (int, float)): + scale_x, scale_y = scale_factor, scale_factor + elif isinstance(scale_factor, Vector2): + scale_x, scale_y = scale_factor.x, scale_factor.y + else: + raise ValueError(f'Unable to scale shape_pts with scale_factor: {scale_factor}') + this_loop = [Point3(v.x * scale_x, v.y * scale_y, v.z) for v in points] + return this_loop + +def _end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]: # Assume points are a basically planar, basically convex polygon with polyhedron # indices `vertex_indices`. # Return a new point that is the centroid of the polygon and a list of From 020bf0647ba58705c288234bc6a6d04d1676fb22 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 16:27:50 -0600 Subject: [PATCH 48/90] Unit tests pass --- solid/test/test_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 9d9c12e5..026476eb 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -28,9 +28,9 @@ ('arc', arc, [10, 0, 90, 24], '\n\ndifference() {\n\tcircle($fn = 24, r = 10);\n\trotate(a = 0) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n\trotate(a = -90) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n}'), ('arc_inverted', arc_inverted, [10, 0, 90, 24], '\n\ndifference() {\n\tintersection() {\n\t\trotate(a = 0) {\n\t\t\ttranslate(v = [-990, 0, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t\trotate(a = 90) {\n\t\t\ttranslate(v = [-990, -1000, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t}\n\tcircle($fn = 24, r = 10);\n}'), ('transform_to_point_scad', transform_to_point, [cube(2), [2, 2, 2], [3, 3, 1]], '\n\nmultmatrix(m = [[0.7071067812, -0.1622214211, -0.6882472016, 2], [-0.7071067812, -0.1622214211, -0.6882472016, 2], [0.0000000000, 0.9733285268, -0.2294157339, 2], [0, 0, 0, 1.0000000000]]) {\n\tcube(size = 2);\n}'), - ('extrude_along_path', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000]]);'), - ('extrude_along_path_xy_scale', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]], [Point2(0.5, 1.5), Point2(1.5, 0.5)]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [5.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 15.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [15.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 5.0000000000]]);'), - ('extrude_along_path_vertical', extrude_along_path, [tri, [[0, 0, 0], [0, 0, 20]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [0, 2, 5], [0, 5, 3], [0, 1, 2], [3, 5, 4]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [-10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 10.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [-10.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, 10.0000000000, 20.0000000000]]);'), + ('extrude_along_path', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);'), + ('extrude_along_path_vertical', extrude_along_path, [tri, [[0, 0, 0], [0, 0, 20]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [-10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 10.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [-10.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, 10.0000000000, 20.0000000000], [-3.3333333333, 3.3333333333, 0.0000000000], [-3.3333333333, 3.3333333333, 20.0000000000]]);'), + ('extrude_along_path_xy_scale', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]], [Point2(0.5, 1.5), Point2(1.5, 0.5)]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [5.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 15.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [15.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 5.0000000000], [1.6666666667, 0.0000000000, 5.0000000000], [5.0000000000, 20.0000000000, 1.6666666667]]);'), ] From 02c5a338eaf507ac29306213bfd365496fc849d8 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 17:45:07 -0600 Subject: [PATCH 49/90] mypy ignore OpenSCAD-imported code --- solid/test/test_solidpython.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 7388f021..308dc74f 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -182,7 +182,8 @@ def test_parse_scad_callables(self): def test_use(self): include_file = self.expand_scad_path("examples/scad_to_include.scad") use(include_file) - a = steps(3) + + a = steps(3) # type: ignore actual = scad_render(a) abs_path = a._get_include_path(include_file) @@ -245,12 +246,12 @@ def test_use_reserved_words(self): f.write(scad_str) use(path) - a = reserved_word_arg(or_=5) + a = reserved_word_arg(or_=5) # type: ignore actual = scad_render(a) expected = f"use <{path}>\n\n\nreserved_word_arg(or = 5);" self.assertEqual(expected, actual) - b = or_(arg=5) + b = or_(arg=5) # type: ignore actual = scad_render(b) expected = f"use <{path}>\n\n\nor(arg = 5);" self.assertEqual(expected, actual) From 9bb399885ece46174bb8271aa05233ecaa9f61af Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 17:45:28 -0600 Subject: [PATCH 50/90] More comprehensive solid.utils.extrude_along_path() testing --- solid/test/test_utils.py | 54 ++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 026476eb..c0b15517 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -28,10 +28,6 @@ ('arc', arc, [10, 0, 90, 24], '\n\ndifference() {\n\tcircle($fn = 24, r = 10);\n\trotate(a = 0) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n\trotate(a = -90) {\n\t\ttranslate(v = [0, -10, 0]) {\n\t\t\tsquare(center = true, size = [30, 20]);\n\t\t}\n\t}\n}'), ('arc_inverted', arc_inverted, [10, 0, 90, 24], '\n\ndifference() {\n\tintersection() {\n\t\trotate(a = 0) {\n\t\t\ttranslate(v = [-990, 0, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t\trotate(a = 90) {\n\t\t\ttranslate(v = [-990, -1000, 0]) {\n\t\t\t\tsquare(center = false, size = [1000, 1000]);\n\t\t\t}\n\t\t}\n\t}\n\tcircle($fn = 24, r = 10);\n}'), ('transform_to_point_scad', transform_to_point, [cube(2), [2, 2, 2], [3, 3, 1]], '\n\nmultmatrix(m = [[0.7071067812, -0.1622214211, -0.6882472016, 2], [-0.7071067812, -0.1622214211, -0.6882472016, 2], [0.0000000000, 0.9733285268, -0.2294157339, 2], [0, 0, 0, 1.0000000000]]) {\n\tcube(size = 2);\n}'), - ('extrude_along_path', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);'), - ('extrude_along_path_vertical', extrude_along_path, [tri, [[0, 0, 0], [0, 0, 20]]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [-10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 10.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [-10.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, 10.0000000000, 20.0000000000], [-3.3333333333, 3.3333333333, 0.0000000000], [-3.3333333333, 3.3333333333, 20.0000000000]]);'), - ('extrude_along_path_xy_scale', extrude_along_path, [tri, [[0, 0, 0], [0, 20, 0]], [Point2(0.5, 1.5), Point2(1.5, 0.5)]], '\n\npolyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [5.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 15.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [15.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 5.0000000000], [1.6666666667, 0.0000000000, 5.0000000000], [5.0000000000, 20.0000000000, 1.6666666667]]);'), - ] other_test_cases = [ @@ -143,15 +139,52 @@ def test_path_2d_polygon(self): expected = [[0,1,2,3],[4,5,6,7]] actual = poly.params['paths'] self.assertEqual(expected, actual) - - # def test_offset_points_inside(self): - # expected = '' - # actual = scad_render(offset_points(tri2d, offset=2, internal=True)) - # self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path(self): + path = [[0, 0, 0], [0, 20, 0]] + # basic test + actual = scad_render(extrude_along_path(tri, path)) + expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' + self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path_vertical(self): + # make sure we still look good extruding along z axis; gimbal lock can mess us up + vert_path = [[0, 0, 0], [0, 0, 20]] + actual = scad_render(extrude_along_path(tri, vert_path)) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[-10.0000000000,0.0000000000,0.0000000000],[0.0000000000,10.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[-10.0000000000,0.0000000000,20.0000000000],[0.0000000000,10.0000000000,20.0000000000],[-3.3333333333,3.3333333333,0.0000000000],[-3.3333333333,3.3333333333,20.0000000000]]);' + self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path_1d_scale(self): + # verify that we can apply scalar scaling + path = [[0, 0, 0], [0, 20, 0]] + scale_factors_1d = [1.5, 0.5] + actual = scad_render(extrude_along_path(tri, path, scale_factors=scale_factors_1d)) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[15.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,15.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,5.0000000000],[5.0000000000,0.0000000000,5.0000000000],[1.6666666667,20.0000000000,1.6666666667]]);' + self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path_2d_scale(self): + # verify that we can apply differential x & y scaling + path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] + scale_factors_2d = [Point2(1,1), Point2(0.5, 1.5), Point2(1.5, 0.5), ] + actual = scad_render(extrude_along_path(tri, path, scale_factors=scale_factors_2d)) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' + self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path_end_caps(self): + path = [[0, 0, 0], [0, 20, 0]] + actual = scad_render(extrude_along_path(tri, path, connect_ends=False)) + expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' + self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path_connect_ends(self): + path = [[0, 0, 0], [20, 0, 0], [20,20,0], [0,20, 0]] + actual = scad_render(extrude_along_path(tri, path, connect_ends=True)) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[6,9,7],[7,9,10],[7,10,8],[8,10,11],[8,11,6],[6,11,9],[0,9,1],[1,9,10],[1,10,2],[2,10,11],[2,11,0],[0,11,9]],points=[[0.0000000000,0.0000000000,0.0000000000],[-7.0710678119,-7.0710678119,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[27.0710678119,-7.0710678119,0.0000000000],[20.0000000000,0.0000000000,10.0000000000],[20.0000000000,20.0000000000,0.0000000000],[27.0710678119,27.0710678119,0.0000000000],[20.0000000000,20.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[-7.0710678119,27.0710678119,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]);' + self.assertEqualNoWhitespace(expected, actual) def test_extrude_along_path_numpy(self): try: - import numpy as np + import numpy as np # type: ignore except ImportError: return @@ -164,7 +197,6 @@ def test_extrude_along_path_numpy(self): # in earlier code, this would have thrown an exception a = extrude_along_path(shape_pts=profile, path_pts=path, scale_factors=scalepts) - def test_label(self): expected = 'translate(v=[0,5.0000000000,0]){resize(newsize=[15,0,0.5000000000]){union(){translate(v=[0,0.0000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="Hello,",valign="baseline");}}translate(v=[0,-11.5000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="World",valign="baseline");}}}}}' actual = scad_render(label("Hello,\nWorld")) From 787c66e5fd6274a12ec40a4e7fefae43ea50a10e Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 17:50:14 -0600 Subject: [PATCH 51/90] type:ignore stragglers --- solid/test/test_solidpython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 308dc74f..ec2a72eb 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -262,7 +262,7 @@ def test_include(self): include_file = self.expand_scad_path("examples/scad_to_include.scad") self.assertIsNotNone(include_file, 'examples/scad_to_include.scad not found') include(include_file) - a = steps(3) + a = steps(3) # type: ignore actual = scad_render(a) abs_path = a._get_include_path(include_file) @@ -434,7 +434,7 @@ def test_scad_render_to_file(self): def test_numpy_type(self): try: - import numpy + 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) From 1a8ea154662eb6db7610026f058773d8315c7826 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 17:53:58 -0600 Subject: [PATCH 52/90] minor README fixes --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f11360d1..0a98908a 100644 --- a/README.rst +++ b/README.rst @@ -130,7 +130,7 @@ Using SolidPython from solid.utils import * # Not required, but the utils module is useful (See `this issue `__ for - a discussion of other import styles + a discussion of other import styles) - OpenSCAD uses curly-brace blocks ({}) to create its tree. SolidPython uses parentheses with comma-delimited lists. @@ -168,7 +168,7 @@ Importing OpenSCAD code ======================= - Use ``solid.import_scad(path)`` to import OpenSCAD code. Relative paths will -check current location designated `OpenSCAD library directories `. +check the current location designated in `OpenSCAD library directories `__. **Ex:** From e994edd168a6cf4e4548a46525849858e42bff6b Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 8 Mar 2021 18:04:41 -0600 Subject: [PATCH 53/90] -- Allow list of 2-tuples as `scale_factors` arguments to solid.utils.extrude_along_path(), as well as Point2s -- Tests for same --- solid/test/test_utils.py | 8 ++++++++ solid/utils.py | 17 ++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index c0b15517..0b3d5ef5 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -170,6 +170,14 @@ def test_extrude_along_path_2d_scale(self): expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' self.assertEqualNoWhitespace(expected, actual) + def test_extrude_along_path_2d_scale_list_input(self): + # verify that we can apply differential x & y scaling + path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] + scale_factors_2d = [(1,1), (0.5, 1.5), (1.5, 0.5), ] + actual = scad_render(extrude_along_path(tri, path, scale_factors=scale_factors_2d)) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' + self.assertEqualNoWhitespace(expected, actual) + def test_extrude_along_path_end_caps(self): path = [[0, 0, 0], [0, 20, 0]] actual = scad_render(extrude_along_path(tri, path, connect_ends=False)) diff --git a/solid/utils.py b/solid/utils.py index c7e1eec8..abb0e15d 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -1143,7 +1143,7 @@ def path_2d_polygon(points:Sequence[Point23], width:float=1, closed:bool=False) # ========================== def extrude_along_path( shape_pts:Points, path_pts:Points, - scale_factors:Sequence[Union[Vector2, float]]=None, + scale_factors:Sequence[Union[Vector2, float, Tuple2]]=None, connect_ends = False) -> OpenSCADObject: # Extrude the convex curve defined by shape_pts along path_pts. # -- For predictable results, shape_pts must be planar, convex, and lie @@ -1234,16 +1234,11 @@ def _loop_facet_indices(loop_start_index:int, loop_pt_count:int, next_loop_start facet_indices.append((b,c,d)) return facet_indices -def _scale_loop(points:Sequence[Point3], scale_factor:Union[float, Point2]=None) -> List[Point3]: - scale_x, scale_y = [1, 1] - if scale_factor: - if isinstance(scale_factor, (int, float)): - scale_x, scale_y = scale_factor, scale_factor - elif isinstance(scale_factor, Vector2): - scale_x, scale_y = scale_factor.x, scale_factor.y - else: - raise ValueError(f'Unable to scale shape_pts with scale_factor: {scale_factor}') - this_loop = [Point3(v.x * scale_x, v.y * scale_y, v.z) for v in points] +def _scale_loop(points:Sequence[Point3], scale:Union[float, Point2, Tuple2]=None) -> List[Point3]: + scale = scale or [1, 1] + if isinstance(scale, (float, int)): + scale = [scale] * 2 + this_loop = [Point3(v.x * scale[0], v.y * scale[1], v.z) for v in points] return this_loop def _end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]: From 29618eb5eb61d20dbec905defd26624ab0b5b82e Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 10 Mar 2021 17:54:52 -0600 Subject: [PATCH 54/90] -- Separated out `solid.utils.extrude_along_path()` into its own file. -- Added custom scale, rotation, and arbitrary transform arguments -- Extended examples to illustrate new behaviors -- Testing --- README.rst | 6 +- solid/examples/path_extrude_example.py | 171 +++++++++++++++++---- solid/extrude_along_path.py | 199 +++++++++++++++++++++++++ solid/test/test_extrude_along_path.py | 123 +++++++++++++++ solid/test/test_utils.py | 90 ++--------- solid/utils.py | 175 +++++----------------- 6 files changed, 528 insertions(+), 236 deletions(-) create mode 100644 solid/extrude_along_path.py create mode 100755 solid/test/test_extrude_along_path.py diff --git a/README.rst b/README.rst index 0a98908a..e9a4ba07 100644 --- a/README.rst +++ b/README.rst @@ -380,7 +380,11 @@ rounds. Extrude Along Path ------------------ -``solid.utils.extrude_along_path(shape_pts, path_pts, scale_factors=None)`` +``solid.utils.extrude_along_path()`` is quite powerful. It can do everything that +OpenSCAD's ``linear_extrude() `` and ``rotate_extrude()`` can do, and lots, lots more. +Scale to custom values throughout the extrusion. Rotate smoothly through the entire +extrusion or specify particular rotations for each step. Apply arbitrary transform +functions to every point in the extrusion. See `solid/examples/path_extrude_example.py `__ diff --git a/solid/examples/path_extrude_example.py b/solid/examples/path_extrude_example.py index 7578895e..51006578 100755 --- a/solid/examples/path_extrude_example.py +++ b/solid/examples/path_extrude_example.py @@ -1,54 +1,60 @@ #! /usr/bin/env python3 +from solid.objects import linear_extrude from solid.solidpython import OpenSCADObject import sys from math import cos, radians, sin, pi, tau from pathlib import Path -from euclid3 import Point2, Point3 +from euclid3 import Point2, Point3, Vector3 -from solid import scad_render_to_file, text, translate -from solid.utils import extrude_along_path, right +from solid import scad_render_to_file, text, translate, cube, color, rotate +from solid.utils import UP_VEC, Vector23, distribute_in_grid, extrude_along_path +from solid.utils import down, right, frange, lerp -from typing import Set, Sequence, List, Callable, Optional, Union, Iterable +from typing import Set, Sequence, List, Callable, Optional, Union, Iterable, Tuple SEGMENTS = 48 +PATH_RAD = 50 +SHAPE_RAD = 15 + +TEXT_LOC = [-0.6 *PATH_RAD, 1.6 * PATH_RAD] def basic_extrude_example(): - path_rad = 50 + path_rad = PATH_RAD shape = star(num_points=5) path = sinusoidal_ring(rad=path_rad, segments=240) + # At its simplest, just sweep a shape along a path extruded = extrude_along_path( shape_pts=shape, path_pts=path) - # Label - extruded += translate([-path_rad/2, 2*path_rad])(text('Basic Extrude')) + extruded += make_label('Basic Extrude') return extruded def extrude_example_xy_scaling() -> OpenSCADObject: num_points = SEGMENTS - path_rad = 50 + path_rad = PATH_RAD circle = circle_points(15) path = circle_points(rad = path_rad) - # angle: from 0 to 6*Pi - angles = list((i/(num_points - 1)*tau*3 for i in range(len(path)))) - - # If scale_factors aren't included, they'll default to + # If scales aren't included, they'll default to # no scaling at each step along path. - no_scale_obj = translate([-path_rad / 2, 2 * path_rad])(text('No Scale')) + no_scale_obj = make_label('No Scale') no_scale_obj += extrude_along_path(circle, path) + # angles: from 0 to 6*Pi + angles = list((frange(0, 3*tau, num_steps=len(path)))) + # With a 1-D scale factor, an extrusion grows and shrinks uniformly x_scales = [(1 + cos(a)/2) for a in angles] - x_obj = translate([-path_rad / 2, 2 * path_rad])(text('1D Scale')) - x_obj += extrude_along_path(circle, path, scale_factors=x_scales) + x_obj = make_label('1D Scale') + x_obj += extrude_along_path(circle, path, scales=x_scales) # With a 2D scale factor, a shape's X & Y dimensions can scale # independently, leading to more interesting shapes # X & Y scales vary between 0.5 & 1.5 xy_scales = [Point2( 1 + cos(a)/2, 1 + sin(a)/2) for a in angles] - xy_obj = translate([-path_rad / 2, 2 * path_rad])( text('2D Scale')) - xy_obj += extrude_along_path(circle, path, scale_factors=xy_scales) + xy_obj = make_label('2D Scale') + xy_obj += extrude_along_path(circle, path, scales=xy_scales) obj = no_scale_obj + right(3*path_rad)(x_obj) + right(6 * path_rad)(xy_obj) return obj @@ -57,20 +63,117 @@ def extrude_example_capped_ends() -> OpenSCADObject: num_points = SEGMENTS/2 path_rad = 50 circle = star(6) - path = circle_points(rad = path_rad) + path = circle_points(rad = path_rad)[:-4] # If `connect_ends` is False or unspecified, ends will be capped. # Endcaps will be correct for most convex or mildly concave (e.g. stars) cross sections - capped_obj = translate([-path_rad / 2, 2 * path_rad])(text('Capped Ends')) - capped_obj += extrude_along_path(circle, path, connect_ends=False) + capped_obj = make_label('Capped Ends') + capped_obj += extrude_along_path(circle, path, connect_ends=False, cap_ends=True) # If `connect_ends` is specified, create a continuous manifold object - connected_obj = translate([-path_rad / 2, 2 * path_rad])(text('Connected Ends')) + connected_obj = make_label('Connected Ends') connected_obj += extrude_along_path(circle, path, connect_ends=True) return capped_obj + right(3*path_rad)(connected_obj) -def sinusoidal_ring(rad=25, segments=SEGMENTS): +def extrude_example_rotations() -> OpenSCADObject: + path_rad = PATH_RAD + shape = star(num_points=5) + path = circle_points(path_rad, num_points=240) + + # For a simple example, make one complete revolution by the end of the extrusion + simple_rot = make_label('Simple Rotation') + simple_rot += extrude_along_path(shape, path, rotations=[360], connect_ends=True) + + # For a more complex set of rotations, add a rotation degree for each point in path + complex_rotations = [] + degs = 0 + oscillation_max = 60 + + for i in frange(0, 1, num_steps=len(path)): + # For the first third of the path, do one complete rotation + if i <= 0.333: + degs = i/0.333*360 + # For the second third of the path, oscillate between +/- oscillation_max degrees + elif i <= 0.666: + angle = lerp(i, 0.333, 0.666, 0, 2*tau) + degs = oscillation_max * sin(angle) + # For the last third of the path, oscillate increasingly fast but with smaller magnitude + else: + # angle increases in a nonlinear curve, so + # oscillations should get quicker and quicker + x = lerp(i, 0.666, 1.0, 0, 2) + angle = pow(x, 2.2) * tau + # decrease the size of the oscillations by a factor of 10 + # over the course of this stretch + osc = lerp(i, 0.666, 1.0, oscillation_max, oscillation_max/10) + degs = osc * sin(angle) + complex_rotations.append(degs) + + complex_rot = make_label('Complex Rotation') + complex_rot += extrude_along_path(shape, path, rotations=complex_rotations) + + # Make some red markers to show the boundaries between the three sections of this path + marker_w = SHAPE_RAD * 1.5 + marker = translate([path_rad, 0, 0])( + cube([marker_w, 1, marker_w], center=True) + ) + markers = [color('red')(rotate([0,0,120*i])(marker)) for i in range(3)] + complex_rot += markers + + return simple_rot + right(3*path_rad)(complex_rot) + +def extrude_example_transforms() -> OpenSCADObject: + path_rad = PATH_RAD + height = 2*SHAPE_RAD + num_steps = 120 + + shape = circle_points(rad=path_rad, num_points=120) + path = [Point3(0,0,i) for i in frange(0, height, num_steps=num_steps)] + + max_rotation = radians(15) + max_z_displacement = height/10 + up = Vector3(0,0,1) + + # The transforms argument is powerful. + # Each point in the entire extrusion will call this function with unique arguments: + # -- `path_norm` in [0, 1] specifying how far along in the extrusion a point's loop is + # -- `loop_norm` in [0, 1] specifying where in its loop a point is. + def point_trans(point: Point3, path_norm:float, loop_norm: float) -> Point3: + # scale the point from 1x to 2x in the course of the + # extrusion, + scale = 1 + path_norm*path_norm/2 + p = scale * point + + # Rotate the points sinusoidally up to max_rotation + p = p.rotate_around(up, max_rotation*sin(tau*path_norm)) + + + # Oscillate z values sinusoidally, growing from + # 0 magnitude to max_z_displacement + max_z = lerp(path_norm, 0, 1, 0, max_z_displacement) + angle = lerp(loop_norm, 0, 1, 0, 10*tau) + p.z += max_z*sin(angle) + return p + + no_trans = make_label('No Transform') + no_trans += down(height/2)( + extrude_along_path(shape, path, cap_ends=False) + ) + + # We can pass transforms a single function that will be called on all points, + # or pass a list with a transform function for each point along path + arb_trans = make_label('Arbitrary Transform') + arb_trans += down(height/2)( + extrude_along_path(shape, path, transforms=[point_trans], cap_ends=False) + ) + + return no_trans + right(3*path_rad)(arb_trans) + +# ============ +# = GEOMETRY = +# ============ +def sinusoidal_ring(rad=25, segments=SEGMENTS) -> List[Point3]: outline = [] for i in range(segments): angle = radians(i * 360 / segments) @@ -83,7 +186,7 @@ def sinusoidal_ring(rad=25, segments=SEGMENTS): outline.append(Point3(x, y, z)) return outline -def star(num_points=5, outer_rad=15, dip_factor=0.5): +def star(num_points=5, outer_rad=SHAPE_RAD, dip_factor=0.5) -> List[Point3]: star_pts = [] for i in range(2 * num_points): rad = outer_rad - i % 2 * dip_factor * outer_rad @@ -91,18 +194,34 @@ def star(num_points=5, outer_rad=15, dip_factor=0.5): star_pts.append(Point3(rad * cos(angle), rad * sin(angle), 0)) return star_pts -def circle_points(rad: float = 15, num_points: int = SEGMENTS) -> List[Point2]: - angles = [tau/num_points * i for i in range(num_points)] +def circle_points(rad: float = SHAPE_RAD, num_points: int = SEGMENTS) -> List[Point2]: + angles = frange(0, tau, num_steps=num_points, include_end=True) points = list([Point2(rad*cos(a), rad*sin(a)) for a in angles]) return points +def make_label(message:str, text_loc:Tuple[float, float]=TEXT_LOC, height=5) -> OpenSCADObject: + return translate(text_loc)( + linear_extrude(height)( + text(message) + ) + ) + +# =============== +# = ENTRY POINT = +# =============== if __name__ == "__main__": out_dir = sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent basic_extrude = basic_extrude_example() scaled_extrusions = extrude_example_xy_scaling() capped_extrusions = extrude_example_capped_ends() - a = basic_extrude + translate([0,-250])(scaled_extrusions) + translate([0, -500])(capped_extrusions) + rotated_extrusions = extrude_example_rotations() + arbitrary_transforms = extrude_example_transforms() + all_objs = [basic_extrude, scaled_extrusions, capped_extrusions, rotated_extrusions, arbitrary_transforms] + + a = distribute_in_grid(all_objs, + max_bounding_box=[4*PATH_RAD, 4*PATH_RAD], + rows_and_cols=[len(all_objs), 1]) file_out = scad_render_to_file(a, out_dir=out_dir, include_orig_code=True) print(f"{__file__}: SCAD file written to: \n{file_out}") diff --git a/solid/extrude_along_path.py b/solid/extrude_along_path.py new file mode 100644 index 00000000..723a503b --- /dev/null +++ b/solid/extrude_along_path.py @@ -0,0 +1,199 @@ +#! /usr/bin/env python +from math import radians +from solid import OpenSCADObject, Points, Indexes, ScadSize, polyhedron +from solid.utils import euclidify, euc_to_arr, transform_to_point, centroid +from euclid3 import Point2, Point3, Vector2, Vector3 + +from typing import Dict, Optional, Sequence, Tuple, Union, List, Callable + +Tuple2 = Tuple[float, float] +FacetIndices = Tuple[int, int, int] +Point3Transform = Callable[[Point3, Optional[float], Optional[float]], Point3] + +# ========================== +# = Extrusion along a path = +# ========================== +def extrude_along_path( shape_pts:Points, + path_pts:Points, + scales:Sequence[Union[Vector2, float, Tuple2]] = None, + rotations: Sequence[float] = None, + transforms: Sequence[Point3Transform] = None, + connect_ends = False, + cap_ends = True) -> OpenSCADObject: + ''' + Extrude the curve defined by shape_pts along path_pts. + -- For predictable results, shape_pts must be planar, convex, and lie + in the XY plane centered around the origin. *Some* nonconvexity (e.g, star shapes) + and nonplanarity will generally work fine + + -- len(scales) should equal len(path_pts). No-op if not supplied + Each entry may be a single number for uniform scaling, or a pair of + numbers (or Point2) for differential X/Y scaling + If not supplied, no scaling will occur. + + -- len(rotations) should equal 1 or len(path_pts). No-op if not supplied. + Each point in shape_pts will be rotated by rotations[i] degrees at + each point in path_pts. Or, if only one rotation is supplied, the shape + will be rotated smoothly over rotations[0] degrees in the course of the extrusion + + -- len(transforms) should be 1 or be equal to len(path_pts). No-op if not supplied. + Each entry should be have the signature: + def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3 + where path_norm is in [0,1] and expresses progress through the extrusion + and loop_norm is in [0,1] and express progress through a single loop of the extrusion + + -- if connect_ends is True, the first and last loops of the extrusion will + be joined, which is useful for toroidal geometries. Overrides cap_ends + + -- if cap_ends is True, each point in the first and last loops of the extrusion + will be connected to the centroid of that loop. For planar, convex shapes, this + works nicely. If shape is less planar or convex, some self-intersection may happen. + Not applied if connect_ends is True + ''' + + + polyhedron_pts:Points= [] + facet_indices:List[Tuple[int, int, int]] = [] + + # Make sure we've got Euclid Point3's for all elements + shape_pts = euclidify(shape_pts, Point3) + path_pts = euclidify(path_pts, Point3) + + src_up = Vector3(0, 0, 1) + + shape_pt_count = len(shape_pts) + + tangent_path_points: List[Point3] = [] + if connect_ends: + tangent_path_points = [path_pts[-1]] + path_pts + [path_pts[0]] + else: + first = Point3(*(path_pts[0] - (path_pts[1] - path_pts[0]))) + last = Point3(*(path_pts[-1] - (path_pts[-2] - path_pts[-1]))) + tangent_path_points = [first] + path_pts + [last] + tangents = [tangent_path_points[i+2] - tangent_path_points[i] for i in range(len(path_pts))] + + for which_loop in range(len(path_pts)): + # path_normal is 0 at the first path_pts and 1 at the last + path_normal = which_loop/ (len(path_pts) - 1) + + path_pt = path_pts[which_loop] + tangent = tangents[which_loop] + scale = scales[which_loop] if scales else 1 + + rotate_degrees = None + if rotations: + rotate_degrees = rotations[which_loop] if len(rotations) > 1 else rotations[0] * path_normal + + transform_func = None + if transforms: + transform_func = transforms[which_loop] if len(transforms) > 1 else transforms[0] + + this_loop = shape_pts[:] + this_loop = _scale_loop(this_loop, scale) + this_loop = _rotate_loop(this_loop, rotate_degrees) + this_loop = _transform_loop(this_loop, transform_func, path_normal) + + this_loop = transform_to_point(this_loop, dest_point=path_pt, dest_normal=tangent, src_up=src_up) + loop_start_index = which_loop * shape_pt_count + + if (which_loop < len(path_pts) - 1): + loop_facets = _loop_facet_indices(loop_start_index, shape_pt_count) + facet_indices += loop_facets + + # Add the transformed points & facets to our final list + polyhedron_pts += this_loop + + if connect_ends: + next_loop_start_index = len(polyhedron_pts) - shape_pt_count + loop_facets = _loop_facet_indices(0, shape_pt_count, next_loop_start_index) + facet_indices += loop_facets + + elif cap_ends: + # endcaps at start & end of extrusion + # NOTE: this block adds points & indices to the polyhedron, so it's + # very sensitive to the order this is happening in + start_cap_index = len(polyhedron_pts) + end_cap_index = start_cap_index + 1 + last_loop_start_index = len(polyhedron_pts) - shape_pt_count + + start_loop_pts = polyhedron_pts[:shape_pt_count] + end_loop_pts = polyhedron_pts[last_loop_start_index:] + + start_loop_indices = list(range(0, shape_pt_count)) + end_loop_indices = list(range(last_loop_start_index, last_loop_start_index + shape_pt_count)) + + start_centroid, start_facet_indices = _end_cap(start_cap_index, start_loop_pts, start_loop_indices) + end_centroid, end_facet_indices = _end_cap(end_cap_index, end_loop_pts, end_loop_indices) + polyhedron_pts += [start_centroid, end_centroid] + facet_indices += start_facet_indices + facet_indices += end_facet_indices + + return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore + +def _loop_facet_indices(loop_start_index:int, loop_pt_count:int, next_loop_start_index=None) -> List[FacetIndices]: + facet_indices: List[FacetIndices] = [] + # nlsi == next_loop_start_index + if next_loop_start_index == None: + next_loop_start_index = loop_start_index + loop_pt_count + loop_indices = list(range(loop_start_index, loop_pt_count + loop_start_index)) + [loop_start_index] + next_loop_indices = list(range(next_loop_start_index, loop_pt_count + next_loop_start_index )) + [next_loop_start_index] + + for i, (a, b) in enumerate(zip(loop_indices[:-1], loop_indices[1:])): + # c--d + # |\ | + # | \| + # a--b + c, d = next_loop_indices[i: i+2] + facet_indices.append((a,c,b)) + facet_indices.append((b,c,d)) + return facet_indices + +def _rotate_loop(points:Sequence[Point3], rotation_degrees:float=None) -> List[Point3]: + if rotation_degrees is None: + return points + up = Vector3(0,0,1) + rads = radians(rotation_degrees) + return [p.rotate_around(up, rads) for p in points] + +def _scale_loop(points:Sequence[Point3], scale:Union[float, Point2, Tuple2]=None) -> List[Point3]: + if scale is None: + return points + + if isinstance(scale, (float, int)): + scale = [scale] * 2 + return [Point3(point.x * scale[0], point.y * scale[1], point.z) for point in points] + +def _transform_loop(points:Sequence[Point3], transform_func:Point3Transform = None, path_normal:float = None) -> List[Point3]: + # transform_func is a function that takes a point and optionally two floats, + # a `path_normal`, in [0,1] that indicates where this loop is in a path extrusion, + # and `loop_normal` in [0,1] that indicates where this point is in a list of points + if transform_func is None: + return points + + result = [] + for i, p in enumerate(points): + # i goes from 0 to 1 across points + loop_normal = i/(len(points) -1) + new_p = transform_func(p, path_normal, loop_normal) + result.append(new_p) + return result + +def _end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]: + # Assume points are a basically planar, basically convex polygon with polyhedron + # indices `vertex_indices`. + # Return a new point that is the centroid of the polygon and a list of + # vertex triangle indices that covers the whole polygon. + # (We can actually accept relatively non-planar and non-convex polygons, + # but not anything pathological. Stars are fine, internal pockets would + # cause incorrect faceting) + + # NOTE: In order to deal with moderately-concave polygons, we add a point + # to the center of the end cap. This will have a new index that we require + # as an argument. + + new_point = centroid(points) + new_facets = [] + second_indices = vertex_indices[1:] + [vertex_indices[0]] + new_facets = [(new_point_index, a, b) for a, b in zip(vertex_indices, second_indices)] + + return (new_point, new_facets) diff --git a/solid/test/test_extrude_along_path.py b/solid/test/test_extrude_along_path.py new file mode 100755 index 00000000..1528f704 --- /dev/null +++ b/solid/test/test_extrude_along_path.py @@ -0,0 +1,123 @@ +#! /usr/bin/env python3 +import unittest +import re + +from solid import OpenSCADObject, scad_render +from solid.utils import extrude_along_path +from euclid3 import Point2, Point3 + +from typing import Union + +tri = [Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)] + +class TestExtrudeAlongPath(unittest.TestCase): + # 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_extrude_along_path(self): + path = [[0, 0, 0], [0, 20, 0]] + # basic test + actual = extrude_along_path(tri, path) + expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_vertical(self): + # make sure we still look good extruding along z axis; gimbal lock can mess us up + vert_path = [[0, 0, 0], [0, 0, 20]] + actual = extrude_along_path(tri, vert_path) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[-10.0000000000,0.0000000000,0.0000000000],[0.0000000000,10.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[-10.0000000000,0.0000000000,20.0000000000],[0.0000000000,10.0000000000,20.0000000000],[-3.3333333333,3.3333333333,0.0000000000],[-3.3333333333,3.3333333333,20.0000000000]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_1d_scale(self): + # verify that we can apply scalar scaling + path = [[0, 0, 0], [0, 20, 0]] + scales_1d = [1.5, 0.5] + actual = extrude_along_path(tri, path, scales=scales_1d) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[15.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,15.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,5.0000000000],[5.0000000000,0.0000000000,5.0000000000],[1.6666666667,20.0000000000,1.6666666667]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_2d_scale(self): + # verify that we can apply differential x & y scaling + path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] + scales_2d = [Point2(1,1), Point2(0.5, 1.5), Point2(1.5, 0.5), ] + actual = extrude_along_path(tri, path, scales=scales_2d) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_2d_scale_list_input(self): + # verify that we can apply differential x & y scaling + path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] + scales_2d = [(1,1), (0.5, 1.5), (1.5, 0.5), ] + actual = extrude_along_path(tri, path, scales=scales_2d) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_end_caps(self): + path = [[0, 0, 0], [0, 20, 0]] + actual = scad_render(extrude_along_path(tri, path, connect_ends=False)) + expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' + self.assertEqualNoWhitespace(expected, actual) + + def test_extrude_along_path_connect_ends(self): + path = [[0, 0, 0], [20, 0, 0], [20,20,0], [0,20, 0]] + actual = extrude_along_path(tri, path, connect_ends=True) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[6,9,7],[7,9,10],[7,10,8],[8,10,11],[8,11,6],[6,11,9],[0,9,1],[1,9,10],[1,10,2],[2,10,11],[2,11,0],[0,11,9]],points=[[0.0000000000,0.0000000000,0.0000000000],[-7.0710678119,-7.0710678119,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[27.0710678119,-7.0710678119,0.0000000000],[20.0000000000,0.0000000000,10.0000000000],[20.0000000000,20.0000000000,0.0000000000],[27.0710678119,27.0710678119,0.0000000000],[20.0000000000,20.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[-7.0710678119,27.0710678119,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_rotations(self): + # confirm we can rotate for each point in path + path = [[0,0,0], [20, 0,0 ]] + rotations = [-45, 45] + actual = extrude_along_path(tri, path, rotations=rotations) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-7.0710678119,-7.0710678119],[0.0000000000,-7.0710678119,7.0710678119],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119],[0.0000000000,-4.7140452079,0.0000000000],[20.0000000000,-0.0000000000,4.7140452079]]);' + self.assertEqualOpenScadObject(expected, actual) + + # confirm we can rotate with a single supplied value + path = [[0,0,0], [20, 0,0 ]] + rotations = [45] + actual = extrude_along_path(tri, path, rotations=rotations) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-10.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119],[0.0000000000,-3.3333333333,3.3333333333],[20.0000000000,-0.0000000000,4.7140452079]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_transforms(self): + path = [[0,0,0], [20, 0,0 ]] + # scale points by a factor of 2 & then 1/2 + # Make sure we can take a transform function for each point in path + transforms = [lambda p, path, loop: 2*p, lambda p, path, loop: 0.5*p] + actual = extrude_along_path(tri, path, transforms=transforms) + expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-5.0000000000,0.0000000000],[20.0000000000,0.0000000000,5.0000000000],[0.0000000000,-6.6666666667,6.6666666667],[20.0000000000,-1.6666666667,1.6666666667]]);' + self.assertEqualOpenScadObject(expected, actual) + + # Make sure we can take a single transform function for all points + transforms = [lambda p, path, loop: 2*p] + actual = extrude_along_path(tri, path, transforms=transforms) + expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, -20.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [20.0000000000, 0.0000000000, 0.0000000000], [20.0000000000, -20.0000000000, 0.0000000000], [20.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, -6.6666666667, 6.6666666667], [20.0000000000, -6.6666666667, 6.6666666667]]);' + self.assertEqualOpenScadObject(expected, actual) + + def test_extrude_along_path_numpy(self): + try: + import numpy as np # type: ignore + except ImportError: + return + + N = 3 + thetas=np.linspace(0,np.pi,N) + path=list(zip(3*np.sin(thetas),3*np.cos(thetas),thetas)) + profile=list(zip(np.sin(thetas),np.cos(thetas), [0]*len(thetas))) + scalepts=list(np.linspace(1,.1,N)) + + # in earlier code, this would have thrown an exception + a = extrude_along_path(shape_pts=profile, path_pts=path, scales=scalepts) + +if __name__ == '__main__': + unittest.main() diff --git a/solid/test/test_utils.py b/solid/test/test_utils.py index 0b3d5ef5..8ba3c2de 100755 --- a/solid/test/test_utils.py +++ b/solid/test/test_utils.py @@ -1,5 +1,6 @@ #! /usr/bin/env python import difflib +from solid.solidpython import OpenSCADObject, scad_render_to_file import unittest import re from euclid3 import Point3, Vector3, Point2 @@ -15,6 +16,8 @@ from solid.utils import back, down, forward, left, right, up from solid.utils import label +from typing import Union + tri = [Point3(0, 0, 0), Point3(10, 0, 0), Point3(0, 10, 0)] scad_test_cases = [ @@ -60,6 +63,13 @@ 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)) @@ -85,19 +95,16 @@ def test_fillet_2d_add(self): pts = [[0, 5], [5, 5], [5, 0], [10, 0], [10, 10], [0, 10], ] p = polygon(pts) three_points = [euclidify(pts[0:3], Point2)] - newp = fillet_2d(three_points, orig_poly=p, fillet_rad=2, remove_material=False) + actual = fillet_2d(three_points, orig_poly=p, fillet_rad=2, remove_material=False) expected = 'union(){polygon(points=[[0,5],[5,5],[5,0],[10,0],[10,10],[0,10]]);translate(v=[3.0000000000,3.0000000000]){difference(){intersection(){rotate(a=359.9000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=450.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' - actual = scad_render(newp) - self.assertEqualNoWhitespace(expected, actual) + self.assertEqualOpenScadObject(expected, actual) def test_fillet_2d_remove(self): pts = list((project_to_2D(p) for p in tri)) poly = polygon(euc_to_arr(pts)) - newp = fillet_2d([pts], orig_poly=poly, fillet_rad=2, remove_material=True) + actual = fillet_2d([pts], orig_poly=poly, fillet_rad=2, remove_material=True) expected = 'difference(){polygon(points=[[0,0],[10,0],[0,10]]);translate(v=[5.1715728753,2.0000000000]){difference(){intersection(){rotate(a=-90.1000000000){translate(v=[-998,0,0]){square(center=false,size=[1000,1000]);}}rotate(a=45.1000000000){translate(v=[-998,-1000,0]){square(center=false,size=[1000,1000]);}}}circle(r=2);}}}' - actual = scad_render(newp) - - self.assertEqualNoWhitespace(expected, actual) + self.assertEqualOpenScadObject(expected, actual) def test_euclidify_non_mutating(self): base_tri = [Point2(0, 0), Point2(10, 0), Point2(0, 10)] @@ -140,75 +147,10 @@ def test_path_2d_polygon(self): actual = poly.params['paths'] self.assertEqual(expected, actual) - def test_extrude_along_path(self): - path = [[0, 0, 0], [0, 20, 0]] - # basic test - actual = scad_render(extrude_along_path(tri, path)) - expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' - self.assertEqualNoWhitespace(expected, actual) - - def test_extrude_along_path_vertical(self): - # make sure we still look good extruding along z axis; gimbal lock can mess us up - vert_path = [[0, 0, 0], [0, 0, 20]] - actual = scad_render(extrude_along_path(tri, vert_path)) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[-10.0000000000,0.0000000000,0.0000000000],[0.0000000000,10.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[-10.0000000000,0.0000000000,20.0000000000],[0.0000000000,10.0000000000,20.0000000000],[-3.3333333333,3.3333333333,0.0000000000],[-3.3333333333,3.3333333333,20.0000000000]]);' - self.assertEqualNoWhitespace(expected, actual) - - def test_extrude_along_path_1d_scale(self): - # verify that we can apply scalar scaling - path = [[0, 0, 0], [0, 20, 0]] - scale_factors_1d = [1.5, 0.5] - actual = scad_render(extrude_along_path(tri, path, scale_factors=scale_factors_1d)) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[15.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,15.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,5.0000000000],[5.0000000000,0.0000000000,5.0000000000],[1.6666666667,20.0000000000,1.6666666667]]);' - self.assertEqualNoWhitespace(expected, actual) - - def test_extrude_along_path_2d_scale(self): - # verify that we can apply differential x & y scaling - path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] - scale_factors_2d = [Point2(1,1), Point2(0.5, 1.5), Point2(1.5, 0.5), ] - actual = scad_render(extrude_along_path(tri, path, scale_factors=scale_factors_2d)) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' - self.assertEqualNoWhitespace(expected, actual) - - def test_extrude_along_path_2d_scale_list_input(self): - # verify that we can apply differential x & y scaling - path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] - scale_factors_2d = [(1,1), (0.5, 1.5), (1.5, 0.5), ] - actual = scad_render(extrude_along_path(tri, path, scale_factors=scale_factors_2d)) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' - self.assertEqualNoWhitespace(expected, actual) - - def test_extrude_along_path_end_caps(self): - path = [[0, 0, 0], [0, 20, 0]] - actual = scad_render(extrude_along_path(tri, path, connect_ends=False)) - expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' - self.assertEqualNoWhitespace(expected, actual) - - def test_extrude_along_path_connect_ends(self): - path = [[0, 0, 0], [20, 0, 0], [20,20,0], [0,20, 0]] - actual = scad_render(extrude_along_path(tri, path, connect_ends=True)) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[6,9,7],[7,9,10],[7,10,8],[8,10,11],[8,11,6],[6,11,9],[0,9,1],[1,9,10],[1,10,2],[2,10,11],[2,11,0],[0,11,9]],points=[[0.0000000000,0.0000000000,0.0000000000],[-7.0710678119,-7.0710678119,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[27.0710678119,-7.0710678119,0.0000000000],[20.0000000000,0.0000000000,10.0000000000],[20.0000000000,20.0000000000,0.0000000000],[27.0710678119,27.0710678119,0.0000000000],[20.0000000000,20.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[-7.0710678119,27.0710678119,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]);' - self.assertEqualNoWhitespace(expected, actual) - - def test_extrude_along_path_numpy(self): - try: - import numpy as np # type: ignore - except ImportError: - return - - N = 3 - thetas=np.linspace(0,np.pi,N) - path=list(zip(3*np.sin(thetas),3*np.cos(thetas),thetas)) - profile=list(zip(np.sin(thetas),np.cos(thetas), [0]*len(thetas))) - scalepts=list(np.linspace(1,.1,N)) - - # in earlier code, this would have thrown an exception - a = extrude_along_path(shape_pts=profile, path_pts=path, scale_factors=scalepts) - def test_label(self): expected = 'translate(v=[0,5.0000000000,0]){resize(newsize=[15,0,0.5000000000]){union(){translate(v=[0,0.0000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="Hello,",valign="baseline");}}translate(v=[0,-11.5000000000,0]){linear_extrude(height=1){text($fn=40,font="MgOpenModata:style=Bold",halign="left",spacing=1,text="World",valign="baseline");}}}}}' - actual = scad_render(label("Hello,\nWorld")) - self.assertEqualNoWhitespace(expected, actual) + actual = label("Hello,\nWorld") + self.assertEqualOpenScadObject(expected, actual) def test_generator_scad(func, args, expected): diff --git a/solid/utils.py b/solid/utils.py index abb0e15d..52f3c117 100755 --- a/solid/utils.py +++ b/solid/utils.py @@ -6,7 +6,6 @@ from solid import difference, intersection, multmatrix, cylinder, color from solid import text, linear_extrude, resize from solid import run_euclid_patch - from solid import OpenSCADObject, P2, P3, P4, Vec3 , Vec4, Vec34, P3s, P23 from solid import Points, Indexes, ScadSize @@ -24,8 +23,6 @@ Line23 = Union[Line2, Line3] LineSegment23 = Union[LineSegment2, LineSegment3] -FacetIndices = Tuple[int, int, int] - Tuple2 = Tuple[float, float] Tuple3 = Tuple[float, float, float] EucOrTuple = Union[Point3, @@ -135,7 +132,7 @@ def distribute_in_grid(objects:Sequence[OpenSCADObject], ret = [] if rows_and_cols: - grid_w, grid_h = rows_and_cols + grid_h, grid_w = rows_and_cols else: grid_w = grid_h = int(ceil(sqrt(len(objects)))) @@ -947,15 +944,6 @@ def draw_segment(euc_line: Union[Vector3, Line3]=None, # TODO: Make a NamedTuple for LEFT_DIR and RIGHT_DIR LEFT_DIR, RIGHT_DIR = 1,2 -def offset_point(a:Point2, b:Point2, c:Point2, offset:float, direction:DirectionLR=LEFT_DIR) -> Point2: - ab_perp = perpendicular_vector(b-a, direction, length=offset) - bc_perp = perpendicular_vector(c-b, direction, length=offset) - - ab_par = Line2(a + ab_perp, b + ab_perp) - bc_par = Line2(b + bc_perp, c + bc_perp) - result = ab_par.intersect(bc_par) - return result - def offset_points(points:Sequence[Point23], offset:float, internal:bool=True, @@ -1005,6 +993,15 @@ def offset_points(points:Sequence[Point23], intersections.append(lines[-1].p + lines[-1].v) return intersections +def offset_point(a:Point2, b:Point2, c:Point2, offset:float, direction:DirectionLR=LEFT_DIR) -> Point2: + ab_perp = perpendicular_vector(b-a, direction, length=offset) + bc_perp = perpendicular_vector(c-b, direction, length=offset) + + ab_par = Line2(a + ab_perp, b + ab_perp) + bc_par = Line2(b + bc_perp, c + bc_perp) + result = ab_par.intersect(bc_par) + return result + # ================== # = Offset helpers = # ================== @@ -1138,130 +1135,20 @@ def path_2d_polygon(points:Sequence[Point23], width:float=1, closed:bool=False) paths = [list(range(len(points))), list(range(len(points), len(path_points)))] return polygon(path_points, paths=paths) -# ========================== -# = Extrusion along a path = -# ========================== -def extrude_along_path( shape_pts:Points, - path_pts:Points, - scale_factors:Sequence[Union[Vector2, float, Tuple2]]=None, - connect_ends = False) -> OpenSCADObject: - # Extrude the convex curve defined by shape_pts along path_pts. - # -- For predictable results, shape_pts must be planar, convex, and lie - # in the XY plane centered around the origin. - # - # -- len(scale_factors) should equal len(path_pts). If not present, scale - # will be assumed to be 1.0 for each point in path_pts - # -- Future additions might include corner styles (sharp, flattened, round) - # or a twist factor - polyhedron_pts:Points= [] - facet_indices:List[Tuple[int, int, int]] = [] - - # Make sure we've got Euclid Point3's for all elements - shape_pts = euclidify(shape_pts, Point3) - path_pts = euclidify(path_pts, Point3) - - src_up = Vector3(*UP_VEC) +# ================= +# = NUMERIC UTILS = +# ================= +def frange(start:float, end:float, num_steps:int=None, step_size:float=1.0, include_end=True): + # if both step_size AND num_steps are supplied, num_steps will be used + step_size = step_size or 1.0 - shape_pt_count = len(shape_pts) - - tangent_path_points: List[Point3] = [] - if connect_ends: - tangent_path_points = [path_pts[-1]] + path_pts + [path_pts[0]] - else: - first = Point3(*(path_pts[0] - (path_pts[1] - path_pts[0]))) - last = Point3(*(path_pts[-1] - (path_pts[-2] - path_pts[-1]))) - tangent_path_points = [first] + path_pts + [last] - tangents = [tangent_path_points[i+2] - tangent_path_points[i] for i in range(len(path_pts))] - - for which_loop in range(len(path_pts)): - path_pt = path_pts[which_loop] - tangent = tangents[which_loop] - scale_factor = scale_factors[which_loop] if scale_factors else 1 - this_loop = shape_pts[:] - this_loop = _scale_loop(this_loop, scale_factor) - this_loop = transform_to_point(this_loop, dest_point=path_pt, dest_normal=tangent, src_up=src_up) - loop_start_index = which_loop * shape_pt_count - - if (which_loop < len(path_pts) - 1): - loop_facets = _loop_facet_indices(loop_start_index, shape_pt_count) - facet_indices += loop_facets - - # Add the transformed points & facets to our final list - polyhedron_pts += this_loop - - if connect_ends: - next_loop_start_index = len(polyhedron_pts) - shape_pt_count - loop_facets = _loop_facet_indices(0, shape_pt_count, next_loop_start_index) - facet_indices += loop_facets + if num_steps: + step_count = num_steps - 1 if include_end else num_steps + step_size = (end - start)/step_count + mode = 3 if include_end else 1 + return _frange_orig(start, end, step_size, mode) - else: - # endcaps at start & end of extrusion - # NOTE: this block adds points & indices to the polyhedron, so it's - # very sensitive to the order this is happening in - start_cap_index = len(polyhedron_pts) - end_cap_index = start_cap_index + 1 - last_loop_start_index = len(polyhedron_pts) - shape_pt_count - - start_loop_pts = polyhedron_pts[:shape_pt_count] - end_loop_pts = polyhedron_pts[last_loop_start_index:] - - start_loop_indices = list(range(0, shape_pt_count)) - end_loop_indices = list(range(last_loop_start_index, last_loop_start_index + shape_pt_count)) - - start_centroid, start_facet_indices = _end_cap(start_cap_index, start_loop_pts, start_loop_indices) - end_centroid, end_facet_indices = _end_cap(end_cap_index, end_loop_pts, end_loop_indices) - polyhedron_pts += [start_centroid, end_centroid] - facet_indices += start_facet_indices - facet_indices += end_facet_indices - - return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore - -def _loop_facet_indices(loop_start_index:int, loop_pt_count:int, next_loop_start_index=None) -> List[FacetIndices]: - facet_indices: List[FacetIndices] = [] - # nlsi == next_loop_start_index - if next_loop_start_index == None: - next_loop_start_index = loop_start_index + loop_pt_count - loop_indices = list(range(loop_start_index, loop_pt_count + loop_start_index)) + [loop_start_index] - next_loop_indices = list(range(next_loop_start_index, loop_pt_count + next_loop_start_index )) + [next_loop_start_index] - - for i, (a, b) in enumerate(zip(loop_indices[:-1], loop_indices[1:])): - # c--d - # |\ | - # | \| - # a--b - c, d = next_loop_indices[i: i+2] - facet_indices.append((a,c,b)) - facet_indices.append((b,c,d)) - return facet_indices - -def _scale_loop(points:Sequence[Point3], scale:Union[float, Point2, Tuple2]=None) -> List[Point3]: - scale = scale or [1, 1] - if isinstance(scale, (float, int)): - scale = [scale] * 2 - this_loop = [Point3(v.x * scale[0], v.y * scale[1], v.z) for v in points] - return this_loop - -def _end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]: - # Assume points are a basically planar, basically convex polygon with polyhedron - # indices `vertex_indices`. - # Return a new point that is the centroid of the polygon and a list of - # vertex triangle indices that covers the whole polygon. - # (We can actually accept relatively non-planar and non-convex polygons, - # but not anything pathological. Stars are fine, internal pockets would - # cause incorrect faceting) - - # NOTE: In order to deal with moderately-concave polygons, we add a point - # to the center of the end cap. This will have a new index that we require - # as an argument. - - new_point = centroid(points) - new_facets = [] - second_indices = vertex_indices[1:] + [vertex_indices[0]] - new_facets = [(new_point_index, a, b) for a, b in zip(vertex_indices, second_indices)] - - return (new_point, new_facets) - -def frange(*args): +def _frange_orig(*args): """ # {{{ http://code.activestate.com/recipes/577068/ (r1) frange([start, ] end [, step [, mode]]) -> generator @@ -1319,6 +1206,18 @@ def frange(*args): i += 1 x = start + i * step +def clamp(val: float, min_val: float, max_val: float) -> float: + result = max(min(val, max_val), min_val) + return result + +def lerp(val: float, min_in: float, max_in: float, min_out: float, max_out: float)-> float: + if min_in == max_in or min_out == max_out: + return min_out + + ratio = (val - min_in) / (max_in - min_in) + result = min_out + ratio * (max_out - min_out); + return result; + # ===================== # = D e b u g g i n g = # ===================== @@ -1347,3 +1246,9 @@ def obj_tree_str(sp_obj:OpenSCADObject, vars_to_print:Sequence[str]=None) -> str s += indent(obj_tree_str(c, vars_to_print)) # type: ignore return s + +# ===================== +# = DEPENDENT IMPORTS = +# ===================== +# imported here to mitigate import loops +from solid.extrude_along_path import extrude_along_path From a5b532e098af5450963b47547ac3a6657851e34a Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 10 Mar 2021 17:58:08 -0600 Subject: [PATCH 55/90] Version 1.1.0 --- pyproject.toml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e528ee1d..11c5bc0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.0.5" +version = "1.1.0" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" @@ -46,13 +46,6 @@ tox = "^tox 3.11" [build-system] requires = [ "poetry>=0.12", - # See https://github.com/pypa/setuptools/issues/2353#issuecomment-683781498 - # for the rest of these requirements, - # -ETJ 31 December 2020 - "setuptools>=30.3.0,<50", - "wheel", - "pytest-runner", - "setuptools_scm>=3.3.1", ] build-backend = "poetry.masonry.api" From cb5fbfffc9e6a02be00638f218f46a02332fd302 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 22 Mar 2021 16:24:10 -0500 Subject: [PATCH 56/90] Restore pyproject.toml hack; Looks like this issue is still unresolved --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 11c5bc0e..0f8cde5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,14 @@ tox = "^tox 3.11" [build-system] requires = [ "poetry>=0.12", + # See https://github.com/pypa/setuptools/issues/2353#issuecomment-683781498 + # for the rest of these requirements, + # -ETJ 31 December 2020 + "setuptools>=30.3.0,<50", + "wheel", + "pytest-runner", + "setuptools_scm>=3.3.1", + ] build-backend = "poetry.masonry.api" From 03fdecd360238c7ddb7608ebb63c5c8dd707041f Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 22 Mar 2021 16:24:47 -0500 Subject: [PATCH 57/90] OpenSCAD's `polygon()` takes a `convexity` argument. Add that argument and a test for it to SP --- solid/objects.py | 6 ++++-- solid/test/test_solidpython.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index 406a3718..b1409e6a 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -36,18 +36,20 @@ class polygon(OpenSCADObject): assumed in order. (The 'pN' components of the *paths* vector are 0-indexed references to the elements of the *points* vector.) + :param convexity: OpenSCAD's convexity... yadda yadda + NOTE: OpenSCAD accepts only 2D points for `polygon()`. Convert any 3D points to 2D before compiling """ - def __init__(self, points: Union[Points, IncludedOpenSCADObject], paths: Indexes = None) -> None: + def __init__(self, points: Union[Points, IncludedOpenSCADObject], paths: Indexes = None, convexity: int = None) -> None: # Force points to 2D if they're defined in Python, pass through if they're # included OpenSCAD code pts = points # type: ignore if not isinstance(points, IncludedOpenSCADObject): pts = list([(p[0], p[1]) for p in points]) # type: ignore - args = {'points':pts} + args = {'points':pts, 'convexity':convexity} # If not supplied, OpenSCAD assumes all points in order for paths if paths: args['paths'] = paths # type: ignore diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index ec2a72eb..47135a10 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -18,6 +18,7 @@ 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]]}, }, {'name': 'polygon', 'class': 'polygon' , 'kwargs': {}, 'expected': '\n\npolygon(points = [[0, 0], [1, 0], [0, 1]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]]}, }, + {'name': 'polygon', 'class': 'polygon' , 'kwargs': {}, 'expected': '\n\npolygon(convexity = 3, points = [[0, 0], [1, 0], [0, 1]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]], 'convexity': 3}, }, {'name': 'circle', 'class': 'circle' , 'kwargs': {'segments': 12, 'r': 1}, 'expected': '\n\ncircle($fn = 12, r = 1);', 'args': {}, }, {'name': 'circle_diam', 'class': 'circle' , 'kwargs': {'segments': 12, 'd': 1}, 'expected': '\n\ncircle($fn = 12, d = 1);', 'args': {}, }, {'name': 'square', 'class': 'square' , 'kwargs': {'center': False, 'size': 1}, 'expected': '\n\nsquare(center = false, size = 1);', 'args': {}, }, From f4608429e91b452ef7ad70241f11d71dbcc5ef3b Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 22 Mar 2021 16:25:31 -0500 Subject: [PATCH 58/90] v1.1.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f8cde5e..05f2dd89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.1.0" +version = "1.1.1" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From cc28da61d8e13b1730b2487fdf6700fc7368942d Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 16:58:50 +0200 Subject: [PATCH 59/90] added py_scadparser --- solid/py_scadparser/LICENSE | 121 ++++++++++++ solid/py_scadparser/README.md | 6 + solid/py_scadparser/scad_parser.py | 297 +++++++++++++++++++++++++++++ solid/py_scadparser/scad_tokens.py | 106 ++++++++++ 4 files changed, 530 insertions(+) create mode 100644 solid/py_scadparser/LICENSE create mode 100644 solid/py_scadparser/README.md create mode 100644 solid/py_scadparser/scad_parser.py create mode 100644 solid/py_scadparser/scad_tokens.py diff --git a/solid/py_scadparser/LICENSE b/solid/py_scadparser/LICENSE new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/solid/py_scadparser/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/solid/py_scadparser/README.md b/solid/py_scadparser/README.md new file mode 100644 index 00000000..29ec2812 --- /dev/null +++ b/solid/py_scadparser/README.md @@ -0,0 +1,6 @@ +# py_scadparser +A basic openscad parser written in python using ply. + +This parser is intended to be used within solidpython to import openscad code. For this purpose we only need to extract the global definitions of a openscad file. That's exactly what this package does. It parses a openscad file and extracts top level definitions. This includes "use"d and "include"d filenames, global variables, function and module definitions. + +Even though this parser actually parses (almost?) the entire openscad language (at least the portions used in my test libraries) 90% is dismissed and only the needed definitions are processed and extracted. diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py new file mode 100644 index 00000000..f7fa4932 --- /dev/null +++ b/solid/py_scadparser/scad_parser.py @@ -0,0 +1,297 @@ +from enum import Enum + +from ply import lex, yacc + +#workaround relative imports.... make this module runable as script +if __name__ == "__main__": + from scad_tokens import * +else: + from .scad_tokens import * + +class ScadTypes(Enum): + GLOBAL_VAR = 0 + MODULE = 1 + FUNCTION = 2 + USE = 3 + INCLUDE = 4 + +class ScadObject: + def __init__(self, scadType): + self.scadType = scadType + + def getType(self): + return self.scadType + +class ScadUse(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.USE) + self.filename = filename + +class ScadInclude(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.INCLUDE) + self.filename = filename + +class ScadGlobalVar(ScadObject): + def __init__(self, name): + super().__init__(ScadTypes.GLOBAL_VAR) + self.name = name + +class ScadModule(ScadObject): + def __init__(self, name, parameters): + super().__init__(ScadTypes.MODULE) + self.name = name + self.parameters = parameters + +class ScadFunction(ScadObject): + def __init__(self, name, parameters): + super().__init__(ScadTypes.FUNCTION) + self.name = name + self.parameters = parameters + +precedence = ( + ('nonassoc', "THEN"), + ('nonassoc', "ELSE"), + ('nonassoc', "?"), + ('nonassoc', ":"), + ('nonassoc', "[", "]", "(", ")", "{", "}"), + + ('nonassoc', '='), + ('left', "AND", "OR"), + ('nonassoc', "EQUAL", "NOT_EQUAL", "GREATER_OR_EQUAL", "LESS_OR_EQUAL", ">", "<"), + ('left', "%"), + ('left', '+', '-'), + ('left', '*', '/'), + ('right', 'NEG', 'POS', 'BACKGROUND', 'NOT'), + ('right', '^'), + ) + +def p_statements(p): + '''statements : statements statement''' + p[0] = p[1] + if p[2] != None: + p[0].append(p[2]) + +def p_statements_empty(p): + '''statements : empty''' + p[0] = [] + +def p_empty(p): + 'empty : ' + +def p_statement(p): + ''' statement : IF "(" expression ")" statement %prec THEN + | IF "(" expression ")" statement ELSE statement + | for_loop statement + | LET "(" assignment_list ")" statement %prec THEN + | "{" statements "}" + | "%" statement %prec BACKGROUND + | "*" statement %prec BACKGROUND + | "!" statement %prec BACKGROUND + | call statement + | ";" + ''' + +def p_for_loop(p): + '''for_loop : FOR "(" parameter_list ")"''' + +def p_statement_use(p): + 'statement : USE FILENAME' + p[0] = ScadUse(p[2][1:len(p[2])-1]) + +def p_statement_include(p): + 'statement : INCLUDE FILENAME' + p[0] = ScadInclude(p[2][1:len(p[2])-1]) + +def p_statement_function(p): + 'statement : function' + p[0] = p[1] + +def p_statement_module(p): + 'statement : module' + p[0] = p[1] + +def p_statement_assignment(p): + 'statement : ID "=" expression ";"' + p[0] = ScadGlobalVar(p[1]) + +def p_expression(p): + '''expression : ID + | expression "." ID + | "-" expression %prec NEG + | "+" expression %prec POS + | "!" expression %prec NOT + | expression "?" expression ":" expression + | expression "%" expression + | expression "+" expression + | expression "-" expression + | expression "/" expression + | expression "*" expression + | expression "^" expression + | expression "<" expression + | expression ">" expression + | expression EQUAL expression + | expression NOT_EQUAL expression + | expression GREATER_OR_EQUAL expression + | expression LESS_OR_EQUAL expression + | expression AND expression + | expression OR expression + | LET "(" assignment_list ")" expression %prec THEN + | EACH expression %prec THEN + | "[" expression ":" expression "]" + | "[" expression ":" expression ":" expression "]" + | "[" for_loop expression "]" + | for_loop expression %prec THEN + | IF "(" expression ")" expression %prec THEN + | IF "(" expression ")" expression ELSE expression + | "(" expression ")" + | call + | expression "[" expression "]" + | tuple + | STRING + | NUMBER''' + +def p_assignment_list(p): + '''assignment_list : ID "=" expression + | assignment_list "," ID "=" expression + ''' + +def p_call(p): + ''' call : ID "(" call_parameter_list ")" + | ID "(" ")"''' + +def p_tuple(p): + ''' tuple : "[" opt_expression_list "]" + ''' + +def p_opt_expression_list(p): + '''opt_expression_list : expression_list + | expression_list "," + | empty''' +def p_expression_list(p): + ''' expression_list : expression_list "," expression + | expression + ''' + +def p_call_parameter_list(p): + '''call_parameter_list : call_parameter_list "," call_parameter + | call_parameter''' + +def p_call_parameter(p): + '''call_parameter : expression + | ID "=" expression''' + +def p_opt_parameter_list(p): + '''opt_parameter_list : parameter_list + | parameter_list "," + | empty + ''' + if p[1] != None: + p[0] = p[1] + else: + p[0] = [] + +def p_parameter_list(p): + '''parameter_list : parameter_list "," parameter + | parameter''' + if len(p) > 2: + p[0] = p[1] + [p[3]] + else: + p[0] = [p[1]] + +def p_parameter(p): + '''parameter : ID + | ID "=" expression''' + p[0] = p[1] + +def p_function(p): + '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression + ''' + + params = None + if p[4] != ")": + params = p[4] + + p[0] = ScadFunction(p[2], params) + +def p_module(p): + '''module : MODULE ID "(" opt_parameter_list ")" statement + ''' + + params = None + if p[4] != ")": + params = p[4] + + p[0] = ScadModule(p[2], params) + +def p_error(p): + print(f'{p.lineno}:{p.lexpos} {p.type} - {p.value}') + print("syntex error") + +def parseFile(scadFile): + from pathlib import Path + p = Path(scadFile) + f = p.open() + + lexer = lex.lex() + parser = yacc.yacc() + + uses = [] + includes = [] + modules = [] + functions = [] + globalVars = [] + + appendObject = { ScadTypes.MODULE : lambda x: modules.append(x), + ScadTypes.FUNCTION: lambda x: functions.append(x), + ScadTypes.GLOBAL_VAR: lambda x: globalVars.append(x), + ScadTypes.USE: lambda x: uses.append(x), + ScadTypes.INCLUDE: lambda x: includes.append(x), + } + + for i in parser.parse(f.read(), lexer=lexer): + appendObject[i.getType()](i) + + return uses, includes, modules, functions, globalVars + +def parseFileAndPrintGlobals(scadFile): + + print(f'======{scadFile}======') + uses, includes, modules, functions, globalVars = parseFile(scadFile) + + print("Uses:") + for u in uses: + print(f' {u.filename}') + + print("Includes:") + for i in includes: + print(f' {i.filename}') + + print("Modules:") + for m in modules: + print(f' {m.name}({m.parameters})') + + print("Functions:") + for m in functions: + print(f' {m.name}({m.parameters})') + + print("Global Vars:") + for m in globalVars: + print(f' {m.name}') + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} [-q] [ ...]\n -q : quiete") + + quiete = sys.argv[1] == "-q" + files = sys.argv[2:] if quiete else sys.argv[1:] + + for i in files: + if quiete: + print(i) + parseFile(i) + else: + parseFileAndPrintGlobals(i) + diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py new file mode 100644 index 00000000..9d69e289 --- /dev/null +++ b/solid/py_scadparser/scad_tokens.py @@ -0,0 +1,106 @@ +literals = [ + ".", ",", ";", + "=", + "!", + ">", "<", + "+", "-", "*", "/", "^", + "?", ":", + "[", "]", "{", "}", "(", ")", + "%", +] + +reserved = { + 'use' : 'USE', + 'include': 'INCLUDE', + 'module' : 'MODULE', + 'function' : 'FUNCTION', + 'if' : 'IF', + 'else' : 'ELSE', + 'for' : 'FOR', + 'let' : 'LET', + 'each' : 'EACH', +} + +tokens = [ + "ID", + "NUMBER", + "STRING", + "EQUAL", + "GREATER_OR_EQUAL", + "LESS_OR_EQUAL", + "NOT_EQUAL", + "AND", "OR", + "FILENAME", + ] + list(reserved.values()) + +#copy & paste from https://github.com/eliben/pycparser/blob/master/pycparser/c_lexer.py +#LICENSE: BSD +simple_escape = r"""([a-wyzA-Z._~!=&\^\-\\?'"]|x(?![0-9a-fA-F]))""" +decimal_escape = r"""(\d+)(?!\d)""" +hex_escape = r"""(x[0-9a-fA-F]+)(?![0-9a-fA-F])""" +bad_escape = r"""([\\][^a-zA-Z._~^!=&\^\-\\?'"x0-9])""" +escape_sequence = r"""(\\("""+simple_escape+'|'+decimal_escape+'|'+hex_escape+'))' +escape_sequence_start_in_string = r"""(\\[0-9a-zA-Z._~!=&\^\-\\?'"])""" +string_char = r"""([^"\\\n]|"""+escape_sequence_start_in_string+')' +t_STRING = '"'+string_char+'*"' + +t_EQUAL = "==" +t_GREATER_OR_EQUAL = ">=" +t_LESS_OR_EQUAL = "<=" +t_NOT_EQUAL = "!=" +t_AND = "\&\&" +t_OR = "\|\|" + +t_FILENAME = r'<[a-zA-Z_0-9/\\\.-]*>' + +t_ignore = "#$" + +def t_eat_escaped_quotes(t): + r"\\\"" + pass + +def t_comments1(t): + r'(/\*(.|\n)*?\*/)' + t.lexer.lineno += t.value.count("\n") + pass + +def t_comments2(t): + r'//.*[\n\']?' + t.lexer.lineno += 1 + pass + +def t_whitespace(t): + r'\s' + t.lexer.lineno += t.value.count("\n") + +def t_ID(t): + r'[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' + t.type = reserved.get(t.value,'ID') + return t + +def t_NUMBER(t): + r'\d*\.?\d+' + t.value = float(t.value) + return t + +def t_error(t): + print(f'Illegal character ({t.lexer.lineno}) "{t.value[0]}"') + t.lexer.skip(1) + +if __name__ == "__main__": + import sys + from ply import lex + from pathlib import Path + + if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} ") + + p = Path(sys.argv[1]) + f = p.open() + lex.lex() + lex.input(''.join(f.readlines())) + for tok in iter(lex.token, None): + if tok.type == "MODULE": + print("") + print(repr(tok.type), repr(tok.value), end='') + From e64dff08b726ce7cab7c1c5414d66a5f1888321b Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:01:13 +0200 Subject: [PATCH 60/90] removed unused function extract_callable_signatures --- solid/solidpython.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index c8b85a33..f1a1b8cd 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -611,9 +611,6 @@ def sp_code_in_scad_comment(calling_file: PathStr) -> str: # =========== # = Parsing = # =========== -def extract_callable_signatures(scad_file_path: PathStr) -> List[dict]: - scad_code_str = Path(scad_file_path).read_text() - return parse_scad_callables(scad_code_str) def parse_scad_callables(scad_code_str: str) -> List[dict]: callables = [] From 49988f22b9e0a7dc2977fc1e8784bb6aa63e368f Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:01:44 +0200 Subject: [PATCH 61/90] use py_scadparser --- solid/objects.py | 11 +--------- solid/solidpython.py | 49 ++++++-------------------------------------- 2 files changed, 7 insertions(+), 53 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index b1409e6a..2307e804 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -853,16 +853,7 @@ def use(scad_file_path: PathStr, use_not_include: bool = True, dest_namespace_di scad_file_path = _find_library(scad_file_path) - contents = None - try: - contents = scad_file_path.read_text() - except Exception as e: - raise Exception(f"Failed to import SCAD module '{scad_file_path}' with error: {e} ") - - # Once we have a list of all callables and arguments, dynamically - # add OpenSCADObject subclasses for all callables to the calling module's - # namespace. - symbols_dicts = parse_scad_callables(contents) + symbols_dicts = parse_scad_callables(scad_file_path) for sd in symbols_dicts: class_str = new_openscad_class_str(sd['name'], sd['args'], sd['kwargs'], diff --git a/solid/solidpython.py b/solid/solidpython.py index f1a1b8cd..9562e8c4 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -611,51 +611,14 @@ def sp_code_in_scad_comment(calling_file: PathStr) -> str: # =========== # = Parsing = # =========== +def parse_scad_callables(filename: str) -> List[dict]: + from .py_scadparser import scad_parser -def parse_scad_callables(scad_code_str: str) -> List[dict]: - callables = [] + _, _, modules, functions, _ = scad_parser.parseFile(filename) - # Note that this isn't comprehensive; tuples or nested data structures in - # a module definition will defeat it. - - # Current implementation would throw an error if you tried to call a(x, y) - # since Python would expect a(x); OpenSCAD itself ignores extra arguments, - # but that's not really preferable behavior - - # TODO: write a pyparsing grammar for OpenSCAD, or, even better, use the yacc parse grammar - # used by the language itself. -ETJ 06 Feb 2011 - - # FIXME: OpenSCAD use/import includes top level variables. We should parse - # those out (e.g. x = someValue;) as well -ETJ 21 May 2019 - no_comments_re = r'(?mxs)(//.*?\n|/\*.*?\*/)' - - # Also note: this accepts: 'module x(arg) =' and 'function y(arg) {', both - # of which are incorrect syntax - mod_re = r'(?mxs)^\s*(?:module|function)\s+(?P\w+)\s*\((?P.*?)\)\s*(?:{|=)' - - # See https://github.com/SolidCode/SolidPython/issues/95; Thanks to https://github.com/Torlos - args_re = r'(?mxs)(?P\w+)(?:\s*=\s*(?P([\w.\"\s\?:\-+\\\/*]+|\((?>[^()]|(?2))*\)|\[(?>[^\[\]]|(?2))*\])+))?(?:,|$)' - - # remove all comments from SCAD code - scad_code_str = re.sub(no_comments_re, '', scad_code_str) - # get all SCAD callables - mod_matches = re.finditer(mod_re, scad_code_str) - - for m in mod_matches: - callable_name = m.group('callable_name') - args = [] - kwargs = [] - all_args = m.group('all_args') - if all_args: - arg_matches = re.finditer(args_re, all_args) - for am in arg_matches: - arg_name = am.group('arg_name') - # NOTE: OpenSCAD's arguments to all functions are effectively - # optional, in contrast to Python in which all args without - # default values are required. - kwargs.append(arg_name) - - callables.append({'name': callable_name, 'args': args, 'kwargs': kwargs}) + callables = [] + for c in modules + functions: + callables.append({'name': c.name, 'args': [], 'kwargs': c.parameters}) return callables From 243bfd78230ca41552e4ee81ca3ad4263e1df41d Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:03:25 +0200 Subject: [PATCH 62/90] fixed wrong openscad function syntax in examples/scad_to_include.scad --- solid/examples/scad_to_include.scad | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index b2c04ba7..34c3414e 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -17,9 +17,7 @@ function scad_points() = [[0,0], [1,0], [0,1]]; // In Python, calling this function without an argument would be an error. // Leave this here to confirm that this works in OpenSCAD. -function optional_nondefault_arg(arg1){ - s = arg1 ? arg1 : 1; - cube([s,s,s]); -} +function optional_nondefault_arg(arg1) = + let(s = arg1 ? arg1 : 1) cube([s,s,s]); echo("This text should appear only when called with include(), not use()"); \ No newline at end of file From c749460076c06154924a90e06a7ed949c69717f0 Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:05:19 +0200 Subject: [PATCH 63/90] added another test module to examples/scad_to_include.scad (and added missing newline before EOF) --- solid/examples/scad_to_include.scad | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index 34c3414e..bc1a033b 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -13,6 +13,8 @@ module steps(howmany=3){ } } +module blub(a) cube([a, 2, 2]); + function scad_points() = [[0,0], [1,0], [0,1]]; // In Python, calling this function without an argument would be an error. @@ -20,4 +22,4 @@ function scad_points() = [[0,0], [1,0], [0,1]]; function optional_nondefault_arg(arg1) = let(s = arg1 ? arg1 : 1) cube([s,s,s]); -echo("This text should appear only when called with include(), not use()"); \ No newline at end of file +echo("This text should appear only when called with include(), not use()"); From fe6b85d23164319d6b83ba69e881bec28f392327 Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 17:15:17 +0200 Subject: [PATCH 64/90] fix OpenSCAD identifiers starting with a digit --- solid/solidpython.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index c8b85a33..de2e60fe 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -739,9 +739,17 @@ def new_openscad_class_str(class_name: str, def _subbed_keyword(keyword: str) -> str: """ Append an underscore to any python reserved word. + Prepend an underscore to any OpenSCAD identifier starting with a digit. No-op for all other strings, e.g. 'or' => 'or_', 'other' => 'other' """ - new_key = keyword + '_' if keyword in PYTHON_ONLY_RESERVED_WORDS else keyword + new_key = keyword + + if keyword in PYTHON_ONLY_RESERVED_WORDS: + new_key = keyword + "_" + + if keyword[0].isdigit(): + new_key = "_" + keyword + if new_key != keyword: print(f"\nFound OpenSCAD code that's not compatible with Python. \n" f"Imported OpenSCAD code using `{keyword}` \n" @@ -751,10 +759,16 @@ def _subbed_keyword(keyword: str) -> str: def _unsubbed_keyword(subbed_keyword: str) -> str: """ Remove trailing underscore for already-subbed python reserved words. + Remove prepending underscore if remaining identifier starts with a digit. No-op for all other strings: e.g. 'or_' => 'or', 'other_' => 'other_' """ - shortened = subbed_keyword[:-1] - return shortened if shortened in PYTHON_ONLY_RESERVED_WORDS else subbed_keyword + if subbed_keyword.endswith("_") and subbed_keyword[:-1] in PYTHON_ONLY_RESERVED_WORDS: + return subbed_keyword[:-1] + + if subbed_keyword.startswith("_") and subbed_keyword[1].isdigit(): + return subbed_keyword[1:] + + return subbed_keyword # now that we have the base class defined, we can do a circular import from . import objects From b883cfa54398ad02cda3fd175cd6ad5a6e2ee766 Mon Sep 17 00:00:00 2001 From: jeff Date: Sun, 16 May 2021 18:15:51 +0200 Subject: [PATCH 65/90] added support for (non)optional arguments --- solid/examples/basic_scad_include.py | 1 + solid/examples/scad_to_include.scad | 2 +- solid/py_scadparser/scad_parser.py | 33 ++++++++++++++++++++-------- solid/solidpython.py | 11 +++++++++- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/solid/examples/basic_scad_include.py b/solid/examples/basic_scad_include.py index 5e432dad..a91020fc 100755 --- a/solid/examples/basic_scad_include.py +++ b/solid/examples/basic_scad_include.py @@ -13,6 +13,7 @@ def demo_import_scad(): scad_path = Path(__file__).parent / 'scad_to_include.scad' scad_mod = import_scad(scad_path) + scad_mod.optional_nondefault_arg(1) return scad_mod.steps(5) diff --git a/solid/examples/scad_to_include.scad b/solid/examples/scad_to_include.scad index bc1a033b..e4e50e33 100644 --- a/solid/examples/scad_to_include.scad +++ b/solid/examples/scad_to_include.scad @@ -13,7 +13,7 @@ module steps(howmany=3){ } } -module blub(a) cube([a, 2, 2]); +module blub(a, b=1) cube([a, 2, 2]); function scad_points() = [[0,0], [1,0], [0,1]]; diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index f7fa4932..08f6d368 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -14,6 +14,7 @@ class ScadTypes(Enum): FUNCTION = 2 USE = 3 INCLUDE = 4 + PARAMETER = 5 class ScadObject: def __init__(self, scadType): @@ -37,17 +38,31 @@ def __init__(self, name): super().__init__(ScadTypes.GLOBAL_VAR) self.name = name -class ScadModule(ScadObject): - def __init__(self, name, parameters): - super().__init__(ScadTypes.MODULE) +class ScadCallable(ScadObject): + def __init__(self, name, parameters, scadType): + super().__init__(scadType) self.name = name self.parameters = parameters -class ScadFunction(ScadObject): + def __repr__(self): + return f'{self.name} ({self.parameters})' + +class ScadModule(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.MODULE) + +class ScadFunction(ScadCallable): def __init__(self, name, parameters): - super().__init__(ScadTypes.FUNCTION) + super().__init__(name, parameters, ScadTypes.FUNCTION) + +class ScadParameter(ScadObject): + def __init__(self, name, optional=False): + super().__init__(ScadTypes.PARAMETER) self.name = name - self.parameters = parameters + self.optional = optional + + def __repr__(self): + return self.name + "=..." if self.optional else self.name precedence = ( ('nonassoc', "THEN"), @@ -202,7 +217,7 @@ def p_parameter_list(p): def p_parameter(p): '''parameter : ID | ID "=" expression''' - p[0] = p[1] + p[0] = ScadParameter(p[1], len(p) == 4) def p_function(p): '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression @@ -269,11 +284,11 @@ def parseFileAndPrintGlobals(scadFile): print("Modules:") for m in modules: - print(f' {m.name}({m.parameters})') + print(f' {m}') print("Functions:") for m in functions: - print(f' {m.name}({m.parameters})') + print(f' {m}') print("Global Vars:") for m in globalVars: diff --git a/solid/solidpython.py b/solid/solidpython.py index 9562e8c4..38ee76a5 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -618,7 +618,16 @@ def parse_scad_callables(filename: str) -> List[dict]: callables = [] for c in modules + functions: - callables.append({'name': c.name, 'args': [], 'kwargs': c.parameters}) + args = [] + kwargs = [] + + for p in c.parameters: + if p.optional: + kwargs.append(p.name) + else: + args.append(p.name) + + callables.append({'name': c.name, 'args': args, 'kwargs': kwargs}) return callables From 9860be43e94000b79096ff8e7143711fba329dc5 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Wed, 19 May 2021 18:56:28 +0200 Subject: [PATCH 66/90] fixed test_parse_scad_callables - parse_scad_callables now receives a filename -> write test_code to tempfile - since the code get written to a file string escapes need to be double escaped ;) - corrected syntax in var_with_functions parameters --- solid/solidpython.py | 12 ++++++++---- solid/test/test_solidpython.py | 21 ++++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 38ee76a5..312c7f0e 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -621,11 +621,15 @@ def parse_scad_callables(filename: str) -> List[dict]: args = [] kwargs = [] + #for some reason solidpython needs to treat all openscad arguments as if + #they where optional. I don't know why, but at least to pass the tests + #it's neccessary to handle it like this !?!?! for p in c.parameters: - if p.optional: - kwargs.append(p.name) - else: - args.append(p.name) + kwargs.append(p.name) + #if p.optional: + # kwargs.append(p.name) + #else: + # args.append(p.name) callables.append({'name': c.name, 'args': args, 'kwargs': kwargs}) diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 47135a10..14fe7349 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -135,17 +135,24 @@ def test_parse_scad_callables(self): module var_number(var_number = -5e89){} module var_empty_vector(var_empty_vector = []){} module var_simple_string(var_simple_string = "simple string"){} - module var_complex_string(var_complex_string = "a \"complex\"\tstring with a\\"){} + module var_complex_string(var_complex_string = "a \\"complex\\"\\tstring with a\\\\"){} module var_vector(var_vector = [5454445, 565, [44545]]){} module var_complex_vector(var_complex_vector = [545 + 4445, 565, [cos(75) + len("yes", 45)]]){} - module var_vector(var_vector = [5, 6, "string\twith\ttab"]){} + module var_vector(var_vector = [5, 6, "string\\twith\\ttab"]){} module var_range(var_range = [0:10e10]){} module var_range_step(var_range_step = [-10:0.5:10]){} module var_with_arithmetic(var_with_arithmetic = 8 * 9 - 1 + 89 / 15){} module var_with_parentheses(var_with_parentheses = 8 * ((9 - 1) + 89) / 15){} - module var_with_functions(var_with_functions = abs(min(chamferHeight2, 0)) */-+ 1){} + module var_with_functions(var_with_functions = abs(min(chamferHeight2, 0)) / 1){} module var_with_conditional_assignment(var_with_conditional_assignment = mytest ? 45 : yop){} + """ + + scad_file = "" + with tempfile.NamedTemporaryFile(suffix=".scad", delete=False) as f: + f.write(test_str.encode("utf-8")) + scad_file = f.name + expected = [ {'name': 'hex', 'args': [], 'kwargs': ['width', 'height', 'flats', 'center']}, {'name': 'righty', 'args': [], 'kwargs': ['angle']}, @@ -177,8 +184,12 @@ def test_parse_scad_callables(self): ] from solid.solidpython import parse_scad_callables - actual = parse_scad_callables(test_str) - self.assertEqual(expected, actual) + actual = parse_scad_callables(scad_file) + + for e in expected: + self.assertEqual(e in actual, True) + + os.unlink(scad_file) def test_use(self): include_file = self.expand_scad_path("examples/scad_to_include.scad") From 1ece137e5802b4486405e6d3975067e53758fff9 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Wed, 19 May 2021 18:57:15 +0200 Subject: [PATCH 67/90] (hopefully) made run_all_tests.sh more portable - I hope this works for everybody else, but I think it should --- solid/test/run_all_tests.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solid/test/run_all_tests.sh b/solid/test/run_all_tests.sh index e99fc6ab..a40ad1a0 100755 --- a/solid/test/run_all_tests.sh +++ b/solid/test/run_all_tests.sh @@ -4,15 +4,16 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" cd $DIR +export PYTHONPATH="../../":$PYTHONPATH # Run all tests. Note that unittest's built-in discovery doesn't run the dynamic # testcase generation they contain for i in test_*.py; do echo $i; - python $i; + python3 $i; echo done # revert to original dir -cd - \ No newline at end of file +cd - From 010c815fcc95364199dfdf33b7cb9182d49e3a82 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Wed, 19 May 2021 19:03:58 +0200 Subject: [PATCH 68/90] fixed unclosed file handle in py_scadparser --- solid/py_scadparser/scad_parser.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index 08f6d368..73e07013 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -244,9 +244,6 @@ def p_error(p): print("syntex error") def parseFile(scadFile): - from pathlib import Path - p = Path(scadFile) - f = p.open() lexer = lex.lex() parser = yacc.yacc() @@ -264,8 +261,10 @@ def parseFile(scadFile): ScadTypes.INCLUDE: lambda x: includes.append(x), } - for i in parser.parse(f.read(), lexer=lexer): - appendObject[i.getType()](i) + from pathlib import Path + with Path(scadFile).open() as f: + for i in parser.parse(f.read(), lexer=lexer): + appendObject[i.getType()](i) return uses, includes, modules, functions, globalVars From d0f6727813067e597ed4e6991aa3afe5c9856a45 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Fri, 21 May 2021 14:29:19 -0500 Subject: [PATCH 69/90] Should resolve #172, where OpenSCAD code was re-imported at every call to `import_scad()`. Now we cache it instead. This matches native Python import semantics better --- solid/objects.py | 22 ++++++++++++++++------ solid/test/test_solidpython.py | 8 ++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index b1409e6a..df50393c 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -22,6 +22,8 @@ ScadSize = Union[int, Sequence[float]] OpenSCADObjectPlus = Union[OpenSCADObject, Sequence[OpenSCADObject]] +IMPORTED_SCAD_MODULES: Dict[Path, SimpleNamespace] = {} + class polygon(OpenSCADObject): """ Create a polygon with the specified points and paths. @@ -761,15 +763,23 @@ def import_scad(scad_file_or_dir: PathStr) -> SimpleNamespace: OpenSCAD files. Create Python mappings for all OpenSCAD modules & functions Return a namespace or raise ValueError if no scad files found ''' + global IMPORTED_SCAD_MODULES + scad = Path(scad_file_or_dir) candidates: List[Path] = [scad] - if not scad.is_absolute(): - candidates = [d/scad for d in _openscad_library_paths()] - for candidate_path in candidates: - namespace = _import_scad(candidate_path) - if namespace is not None: - return namespace + ns = IMPORTED_SCAD_MODULES.get(scad) + if ns: + return ns + else: + if not scad.is_absolute(): + candidates = [d/scad for d in _openscad_library_paths()] + + for candidate_path in candidates: + namespace = _import_scad(candidate_path) + if namespace is not None: + IMPORTED_SCAD_MODULES[scad] = namespace + return namespace raise ValueError(f'Could not find .scad files at or under {scad}. \nLocations searched were: {candidates}') def _import_scad(scad: Path) -> Optional[SimpleNamespace]: diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 47135a10..3ba176f5 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -227,6 +227,14 @@ def test_import_scad(self): # are imported correctly. Not sure how to do this without writing # temp files to those directories. Seems like overkill for the moment + def test_multiple_import_scad(self): + # For Issue #172. Originally, multiple `import_scad()` calls would + # re-import the entire module, rather than cache a module after one use + include_file = self.expand_scad_path("examples/scad_to_include.scad") + mod1 = import_scad(include_file) + mod2 = import_scad(include_file) + self.assertEqual(mod1, mod2) + def test_imported_scad_arguments(self): include_file = self.expand_scad_path("examples/scad_to_include.scad") mod = import_scad(include_file) From 18fe4874917ab228fdc40ae956f74427390a47ee Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Sat, 22 May 2021 22:54:10 +0200 Subject: [PATCH 70/90] allow strings to be quoted by single ticks -> ' - could this maybe fix the "MacOS mcad issue" from #170 --- solid/py_scadparser/scad_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index 9d69e289..f074c2ff 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -42,7 +42,7 @@ escape_sequence = r"""(\\("""+simple_escape+'|'+decimal_escape+'|'+hex_escape+'))' escape_sequence_start_in_string = r"""(\\[0-9a-zA-Z._~!=&\^\-\\?'"])""" string_char = r"""([^"\\\n]|"""+escape_sequence_start_in_string+')' -t_STRING = '"'+string_char+'*"' +t_STRING = '"'+string_char+'*"' + " | " + "'" +string_char+ "*'" t_EQUAL = "==" t_GREATER_OR_EQUAL = ">=" From 7a0f612e105de3a9a07f17c81fe10036c6728e1d Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Sat, 22 May 2021 23:12:21 +0200 Subject: [PATCH 71/90] improved py_scadparser error messages --- solid/py_scadparser/scad_parser.py | 4 ++-- solid/py_scadparser/scad_tokens.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index 73e07013..9200107a 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -240,12 +240,12 @@ def p_module(p): p[0] = ScadModule(p[2], params) def p_error(p): - print(f'{p.lineno}:{p.lexpos} {p.type} - {p.value}') - print("syntex error") + print(f'py_scadparser: Syntax error: {p.lexer.filename}({p.lineno}) {p.type} - {p.value}') def parseFile(scadFile): lexer = lex.lex() + lexer.filename = scadFile parser = yacc.yacc() uses = [] diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index f074c2ff..f79bcc03 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -84,7 +84,7 @@ def t_NUMBER(t): return t def t_error(t): - print(f'Illegal character ({t.lexer.lineno}) "{t.value[0]}"') + print(f'py_scadparser: Illegal character: {t.lexer.filename}({t.lexer.lineno}) "{t.value[0]}"') t.lexer.skip(1) if __name__ == "__main__": @@ -97,9 +97,10 @@ def t_error(t): p = Path(sys.argv[1]) f = p.open() - lex.lex() - lex.input(''.join(f.readlines())) - for tok in iter(lex.token, None): + lexer = lex.lex() + lexer.filename = p.as_posix() + lexer.input(''.join(f.readlines())) + for tok in iter(lexer.token, None): if tok.type == "MODULE": print("") print(repr(tok.type), repr(tok.value), end='') From 2de5803e66c7ba3dd7f7d26b1f5d34967da15d62 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Mon, 24 May 2021 15:00:53 +0200 Subject: [PATCH 72/90] updated py_scadparser it's now based on the openscad/parser.y ;) --- solid/py_scadparser/scad_ast.py | 58 ++++ solid/py_scadparser/scad_parser.py | 445 ++++++++++++++--------------- solid/py_scadparser/scad_tokens.py | 13 +- 3 files changed, 287 insertions(+), 229 deletions(-) create mode 100644 solid/py_scadparser/scad_ast.py diff --git a/solid/py_scadparser/scad_ast.py b/solid/py_scadparser/scad_ast.py new file mode 100644 index 00000000..5e8b49a8 --- /dev/null +++ b/solid/py_scadparser/scad_ast.py @@ -0,0 +1,58 @@ +from enum import Enum + +class ScadTypes(Enum): + GLOBAL_VAR = 0 + MODULE = 1 + FUNCTION = 2 + USE = 3 + INCLUDE = 4 + PARAMETER = 5 + +class ScadObject: + def __init__(self, scadType): + self.scadType = scadType + + def getType(self): + return self.scadType + +class ScadUse(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.USE) + self.filename = filename + +class ScadInclude(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.INCLUDE) + self.filename = filename + +class ScadGlobalVar(ScadObject): + def __init__(self, name): + super().__init__(ScadTypes.GLOBAL_VAR) + self.name = name + +class ScadCallable(ScadObject): + def __init__(self, name, parameters, scadType): + super().__init__(scadType) + self.name = name + self.parameters = parameters + + def __repr__(self): + return f'{self.name} ({self.parameters})' + +class ScadModule(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.MODULE) + +class ScadFunction(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.FUNCTION) + +class ScadParameter(ScadObject): + def __init__(self, name, optional=False): + super().__init__(ScadTypes.PARAMETER) + self.name = name + self.optional = optional + + def __repr__(self): + return self.name + "=None" if self.optional else self.name + diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index 9200107a..cd7e9317 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -1,243 +1,249 @@ -from enum import Enum - from ply import lex, yacc #workaround relative imports.... make this module runable as script if __name__ == "__main__": + from scad_ast import * from scad_tokens import * else: + from .scad_ast import * from .scad_tokens import * -class ScadTypes(Enum): - GLOBAL_VAR = 0 - MODULE = 1 - FUNCTION = 2 - USE = 3 - INCLUDE = 4 - PARAMETER = 5 - -class ScadObject: - def __init__(self, scadType): - self.scadType = scadType - - def getType(self): - return self.scadType - -class ScadUse(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.USE) - self.filename = filename - -class ScadInclude(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.INCLUDE) - self.filename = filename - -class ScadGlobalVar(ScadObject): - def __init__(self, name): - super().__init__(ScadTypes.GLOBAL_VAR) - self.name = name - -class ScadCallable(ScadObject): - def __init__(self, name, parameters, scadType): - super().__init__(scadType) - self.name = name - self.parameters = parameters - - def __repr__(self): - return f'{self.name} ({self.parameters})' - -class ScadModule(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.MODULE) - -class ScadFunction(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.FUNCTION) - -class ScadParameter(ScadObject): - def __init__(self, name, optional=False): - super().__init__(ScadTypes.PARAMETER) - self.name = name - self.optional = optional - - def __repr__(self): - return self.name + "=..." if self.optional else self.name - precedence = ( - ('nonassoc', "THEN"), - ('nonassoc', "ELSE"), - ('nonassoc', "?"), - ('nonassoc', ":"), - ('nonassoc', "[", "]", "(", ")", "{", "}"), - - ('nonassoc', '='), - ('left', "AND", "OR"), - ('nonassoc', "EQUAL", "NOT_EQUAL", "GREATER_OR_EQUAL", "LESS_OR_EQUAL", ">", "<"), - ('left', "%"), - ('left', '+', '-'), - ('left', '*', '/'), - ('right', 'NEG', 'POS', 'BACKGROUND', 'NOT'), - ('right', '^'), - ) - -def p_statements(p): - '''statements : statements statement''' - p[0] = p[1] - if p[2] != None: - p[0].append(p[2]) + ('nonassoc', 'NO_ELSE'), + ('nonassoc', 'ELSE'), + ) -def p_statements_empty(p): - '''statements : empty''' +def p_input(p): + """input :""" p[0] = [] -def p_empty(p): - 'empty : ' +def p_input_use(p): + """input : input USE FILENAME + | input INCLUDE FILENAME""" + p[0] = p[1] + +def p_input_statement(p): + """input : input statement""" + p[0] = p[1] + if p[2] != None: + p[0].append(p[2]) def p_statement(p): - ''' statement : IF "(" expression ")" statement %prec THEN - | IF "(" expression ")" statement ELSE statement - | for_loop statement - | LET "(" assignment_list ")" statement %prec THEN - | "{" statements "}" - | "%" statement %prec BACKGROUND - | "*" statement %prec BACKGROUND - | "!" statement %prec BACKGROUND - | call statement - | ";" - ''' - -def p_for_loop(p): - '''for_loop : FOR "(" parameter_list ")"''' - -def p_statement_use(p): - 'statement : USE FILENAME' - p[0] = ScadUse(p[2][1:len(p[2])-1]) - -def p_statement_include(p): - 'statement : INCLUDE FILENAME' - p[0] = ScadInclude(p[2][1:len(p[2])-1]) + """statement : ';' + | '{' inner_input '}' + | module_instantiation + """ + p[0] = None + +def p_statement_assigment(p): + """statement : assignment""" + p[0] = p[1] def p_statement_function(p): - 'statement : function' - p[0] = p[1] + """statement : MODULE ID '(' parameters optional_commas ')' statement + | FUNCTION ID '(' parameters optional_commas ')' '=' expr ';' + """ + if p[1] == 'module': + p[0] = ScadModule(p[2], p[4]) + elif p[1] == 'function': + p[0] = ScadFunction(p[2], p[4]) + else: + assert(False) -def p_statement_module(p): - 'statement : module' - p[0] = p[1] +def p_inner_input(p): + """inner_input : + | inner_input statement + """ -def p_statement_assignment(p): - 'statement : ID "=" expression ";"' +def p_assignment(p): + """assignment : ID '=' expr ';'""" p[0] = ScadGlobalVar(p[1]) -def p_expression(p): - '''expression : ID - | expression "." ID - | "-" expression %prec NEG - | "+" expression %prec POS - | "!" expression %prec NOT - | expression "?" expression ":" expression - | expression "%" expression - | expression "+" expression - | expression "-" expression - | expression "/" expression - | expression "*" expression - | expression "^" expression - | expression "<" expression - | expression ">" expression - | expression EQUAL expression - | expression NOT_EQUAL expression - | expression GREATER_OR_EQUAL expression - | expression LESS_OR_EQUAL expression - | expression AND expression - | expression OR expression - | LET "(" assignment_list ")" expression %prec THEN - | EACH expression %prec THEN - | "[" expression ":" expression "]" - | "[" expression ":" expression ":" expression "]" - | "[" for_loop expression "]" - | for_loop expression %prec THEN - | IF "(" expression ")" expression %prec THEN - | IF "(" expression ")" expression ELSE expression - | "(" expression ")" - | call - | expression "[" expression "]" - | tuple - | STRING - | NUMBER''' - -def p_assignment_list(p): - '''assignment_list : ID "=" expression - | assignment_list "," ID "=" expression - ''' +def p_module_instantiation(p): + """module_instantiation : '!' module_instantiation + | '#' module_instantiation + | '%' module_instantiation + | '*' module_instantiation + | single_module_instantiation child_statement + | ifelse_statement + """ + +def p_ifelse_statement(p): + """ifelse_statement : if_statement %prec NO_ELSE + | if_statement ELSE child_statement + """ + +def p_if_statement(p): + """if_statement : IF '(' expr ')' child_statement + """ + +def p_child_statements(p): + """child_statements : + | child_statements child_statement + | child_statements assignment + """ + +def p_child_statement(p): + """child_statement : ';' + | '{' child_statements '}' + | module_instantiation + """ + +def p_module_id(p): + """module_id : ID + | FOR + | LET + | ASSERT + | ECHO + | EACH + """ + +def p_single_module_instantiation(p): + """single_module_instantiation : module_id '(' arguments ')' + """ + +def p_expr(p): + """expr : logic_or + | FUNCTION '(' parameters optional_commas ')' expr %prec NO_ELSE + | logic_or '?' expr ':' expr + | LET '(' arguments ')' expr + | ASSERT '(' arguments ')' expr_or_empty + | ECHO '(' arguments ')' expr_or_empty + """ + +def p_logic_or(p): + """logic_or : logic_and + | logic_or OR logic_and + """ + +def p_logic_and(p): + """logic_and : equality + | logic_and AND equality + """ + +def p_equality(p): + """equality : comparison + | equality EQUAL comparison + | equality NOT_EQUAL comparison + """ + +def p_comparison(p): + """comparison : addition + | comparison '>' addition + | comparison GREATER_OR_EQUAL addition + | comparison '<' addition + | comparison LESS_OR_EQUAL addition + """ + +def p_addition(p): + """addition : multiplication + | addition '+' multiplication + | addition '-' multiplication + """ + +def p_multiplication(p): + """multiplication : unary + | multiplication '*' unary + | multiplication '/' unary + | multiplication '%' unary + """ + +def p_unary(p): + """unary : exponent + | '+' unary + | '-' unary + | '!' unary + """ + +def p_exponent(p): + """exponent : call + | call '^' unary + """ def p_call(p): - ''' call : ID "(" call_parameter_list ")" - | ID "(" ")"''' - -def p_tuple(p): - ''' tuple : "[" opt_expression_list "]" - ''' - -def p_opt_expression_list(p): - '''opt_expression_list : expression_list - | expression_list "," - | empty''' -def p_expression_list(p): - ''' expression_list : expression_list "," expression - | expression - ''' - -def p_call_parameter_list(p): - '''call_parameter_list : call_parameter_list "," call_parameter - | call_parameter''' - -def p_call_parameter(p): - '''call_parameter : expression - | ID "=" expression''' - -def p_opt_parameter_list(p): - '''opt_parameter_list : parameter_list - | parameter_list "," - | empty - ''' - if p[1] != None: - p[0] = p[1] - else: - p[0] = [] - -def p_parameter_list(p): - '''parameter_list : parameter_list "," parameter - | parameter''' - if len(p) > 2: - p[0] = p[1] + [p[3]] - else: + """call : primary + | call '(' arguments ')' + | call '[' expr ']' + | call '.' ID + """ + +def p_primary(p): + """primary : TRUE + | FALSE + | UNDEF + | NUMBER + | STRING + | ID + | '(' expr ')' + | '[' expr ':' expr ']' + | '[' expr ':' expr ':' expr ']' + | '[' optional_commas ']' + | '[' vector_expr optional_commas ']' + """ + +def p_expr_or_empty(p): + """expr_or_empty : + | expr + """ + +def p_list_comprehension_elements(p): + """list_comprehension_elements : LET '(' arguments ')' list_comprehension_elements_p + | EACH list_comprehension_elements_or_expr + | FOR '(' arguments ')' list_comprehension_elements_or_expr + | FOR '(' arguments ';' expr ';' arguments ')' list_comprehension_elements_or_expr + | IF '(' expr ')' list_comprehension_elements_or_expr %prec NO_ELSE + | IF '(' expr ')' list_comprehension_elements_or_expr ELSE list_comprehension_elements_or_expr + """ + +def p_list_comprehension_elements_p(p): + """list_comprehension_elements_p : list_comprehension_elements + | '(' list_comprehension_elements ')' + """ + +def p_list_comprehension_elements_or_expr(p): + """list_comprehension_elements_or_expr : list_comprehension_elements_p + | expr + """ + +def p_optional_commas(p): + """optional_commas : + | ',' optional_commas + """ + +def p_vector_expr(p): + """vector_expr : expr + | list_comprehension_elements + | vector_expr ',' optional_commas list_comprehension_elements_or_expr + """ + +def p_parameters(p): + """parameters : + | parameter + | parameters ',' optional_commas parameter + """ + if len(p) == 1: + p[0] = [] + elif len(p) == 2: p[0] = [p[1]] + else: + p[0] = p[1] + [p[4]] def p_parameter(p): - '''parameter : ID - | ID "=" expression''' + """parameter : ID + | ID '=' expr + """ p[0] = ScadParameter(p[1], len(p) == 4) -def p_function(p): - '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression - ''' +def p_arguments(p): + """arguments : + | argument + | arguments ',' optional_commas argument + """ - params = None - if p[4] != ")": - params = p[4] - - p[0] = ScadFunction(p[2], params) - -def p_module(p): - '''module : MODULE ID "(" opt_parameter_list ")" statement - ''' - - params = None - if p[4] != ")": - params = p[4] - - p[0] = ScadModule(p[2], params) +def p_argument(p): + """argument : expr + | ID '=' expr + """ def p_error(p): print(f'py_scadparser: Syntax error: {p.lexer.filename}({p.lineno}) {p.type} - {p.value}') @@ -266,20 +272,12 @@ def parseFile(scadFile): for i in parser.parse(f.read(), lexer=lexer): appendObject[i.getType()](i) - return uses, includes, modules, functions, globalVars + return modules, functions, globalVars def parseFileAndPrintGlobals(scadFile): print(f'======{scadFile}======') - uses, includes, modules, functions, globalVars = parseFile(scadFile) - - print("Uses:") - for u in uses: - print(f' {u.filename}') - - print("Includes:") - for i in includes: - print(f' {i.filename}') + modules, functions, globalVars = parseFile(scadFile) print("Modules:") for m in modules: @@ -289,7 +287,7 @@ def parseFileAndPrintGlobals(scadFile): for m in functions: print(f' {m}') - print("Global Vars:") + print("Global Variables:") for m in globalVars: print(f' {m.name}') @@ -304,7 +302,6 @@ def parseFileAndPrintGlobals(scadFile): for i in files: if quiete: - print(i) parseFile(i) else: parseFileAndPrintGlobals(i) diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index f79bcc03..1bd9d85d 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -6,7 +6,7 @@ "+", "-", "*", "/", "^", "?", ":", "[", "]", "{", "}", "(", ")", - "%", + "%", "#" ] reserved = { @@ -16,9 +16,14 @@ 'function' : 'FUNCTION', 'if' : 'IF', 'else' : 'ELSE', - 'for' : 'FOR', 'let' : 'LET', + 'assert' : 'ASSERT', + 'echo' : 'ECHO', + 'for' : 'FOR', 'each' : 'EACH', + 'true' : 'TRUE', + 'false' : 'FALSE', + 'undef' : 'UNDEF', } tokens = [ @@ -53,8 +58,6 @@ t_FILENAME = r'<[a-zA-Z_0-9/\\\.-]*>' -t_ignore = "#$" - def t_eat_escaped_quotes(t): r"\\\"" pass @@ -74,7 +77,7 @@ def t_whitespace(t): t.lexer.lineno += t.value.count("\n") def t_ID(t): - r'[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' + r'[\$]?[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' t.type = reserved.get(t.value,'ID') return t From ca012e5fd9b4a8e3f235002375c6b7395363e4af Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Mon, 24 May 2021 15:03:33 +0200 Subject: [PATCH 73/90] adjusted solidpython to meet py_scadparser changes - [un]subbed_keyword should now handle $parameters - it should be possible to remove all $fn <-> segments code from everywhere and let [un]subbed_keyword do the work --- solid/solidpython.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/solid/solidpython.py b/solid/solidpython.py index 42026bee..2b56aa34 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -614,7 +614,7 @@ def sp_code_in_scad_comment(calling_file: PathStr) -> str: def parse_scad_callables(filename: str) -> List[dict]: from .py_scadparser import scad_parser - _, _, modules, functions, _ = scad_parser.parseFile(filename) + modules, functions, _ = scad_parser.parseFile(filename) callables = [] for c in modules + functions: @@ -720,9 +720,15 @@ def _subbed_keyword(keyword: str) -> str: if keyword in PYTHON_ONLY_RESERVED_WORDS: new_key = keyword + "_" - if keyword[0].isdigit(): + elif keyword[0].isdigit(): new_key = "_" + keyword + elif keyword == "$fn": + new_key = "segments" + + elif keyword[0] == "$": + new_key = "__" + keyword[1:] + if new_key != keyword: print(f"\nFound OpenSCAD code that's not compatible with Python. \n" f"Imported OpenSCAD code using `{keyword}` \n" @@ -738,9 +744,15 @@ def _unsubbed_keyword(subbed_keyword: str) -> str: if subbed_keyword.endswith("_") and subbed_keyword[:-1] in PYTHON_ONLY_RESERVED_WORDS: return subbed_keyword[:-1] - if subbed_keyword.startswith("_") and subbed_keyword[1].isdigit(): + elif subbed_keyword.startswith("__"): + return "$" + subbed_keyword[2:] + + elif subbed_keyword.startswith("_") and subbed_keyword[1].isdigit(): return subbed_keyword[1:] + elif subbed_keyword == "segments": + return "$fn" + return subbed_keyword # now that we have the base class defined, we can do a circular import From 389bdcd32ad178c3905756d2d72f9ae8f74fcf91 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Mon, 24 May 2021 15:37:13 +0200 Subject: [PATCH 74/90] removed dead code --- solid/py_scadparser/scad_ast.py | 10 ---------- solid/py_scadparser/scad_parser.py | 4 ---- 2 files changed, 14 deletions(-) diff --git a/solid/py_scadparser/scad_ast.py b/solid/py_scadparser/scad_ast.py index 5e8b49a8..9bfc1aa4 100644 --- a/solid/py_scadparser/scad_ast.py +++ b/solid/py_scadparser/scad_ast.py @@ -15,16 +15,6 @@ def __init__(self, scadType): def getType(self): return self.scadType -class ScadUse(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.USE) - self.filename = filename - -class ScadInclude(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.INCLUDE) - self.filename = filename - class ScadGlobalVar(ScadObject): def __init__(self, name): super().__init__(ScadTypes.GLOBAL_VAR) diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index cd7e9317..fe56aa4a 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -254,8 +254,6 @@ def parseFile(scadFile): lexer.filename = scadFile parser = yacc.yacc() - uses = [] - includes = [] modules = [] functions = [] globalVars = [] @@ -263,8 +261,6 @@ def parseFile(scadFile): appendObject = { ScadTypes.MODULE : lambda x: modules.append(x), ScadTypes.FUNCTION: lambda x: functions.append(x), ScadTypes.GLOBAL_VAR: lambda x: globalVars.append(x), - ScadTypes.USE: lambda x: uses.append(x), - ScadTypes.INCLUDE: lambda x: includes.append(x), } from pathlib import Path From f7eb1536bd4ef98035c554f43ee001dba592d107 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 24 May 2021 10:54:44 -0500 Subject: [PATCH 75/90] Added ply as project dependency. Removed regex --- pyproject.toml | 2 +- solid/solidpython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 05f2dd89..3368907b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ python = ">=3.7" euclid3 = "^0.1.0" pypng = "^0.0.19" PrettyTable = "=0.7.2" -regex = "^2019.4" +ply = "^3.11" [tool.poetry.dev-dependencies] tox = "^tox 3.11" diff --git a/solid/solidpython.py b/solid/solidpython.py index 2b56aa34..4fc935f2 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -24,7 +24,7 @@ from typing import Callable, Iterable, List, Optional, Sequence, Set, Union, Dict import pkg_resources -import regex as re +import re PathStr = Union[Path, str] AnimFunc = Callable[[Optional[float]], 'OpenSCADObject'] From 5b269c98a6f0a16018f9574b497298a19bf7359b Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 24 May 2021 11:07:34 -0500 Subject: [PATCH 76/90] Tell `yacc.yacc()` not to output extra files --- solid/py_scadparser/scad_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index fe56aa4a..24e193cc 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -252,7 +252,7 @@ def parseFile(scadFile): lexer = lex.lex() lexer.filename = scadFile - parser = yacc.yacc() + parser = yacc.yacc(debug=False, write_tables=False) modules = [] functions = [] From 8f55fd12293f905e4f5c892b5900b10f0c8c9b43 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 24 May 2021 11:52:56 -0500 Subject: [PATCH 77/90] Prefix an underscore to imported SCAD files starting with a digit, which are valid in OpenSCAD but not in Python. e.g. mcad.2Dshapes => mcad._2dshapes --- solid/objects.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/solid/objects.py b/solid/objects.py index 88540d97..79aff238 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -804,7 +804,13 @@ def _import_scad(scad: Path) -> Optional[SimpleNamespace]: if namespace is None: namespace = SimpleNamespace() # Add a subspace to namespace named by the file/dir it represents - setattr(namespace, f.stem, subspace) + package_name = f.stem + # Prefix an underscore to packages starting with a digit, which + # are valid in OpenSCAD but not in Python + if package_name[0].isdigit(): + package_name = '_' + package_name + + setattr(namespace, package_name, subspace) return namespace From 625078a22021ab292b2446457f3999dae34abb97 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Tue, 25 May 2021 17:44:48 +0200 Subject: [PATCH 78/90] Revert "updated py_scadparser it's now based on the openscad/parser.y ;)" This reverts commit 2de5803e66c7ba3dd7f7d26b1f5d34967da15d62. # Conflicts: # solid/py_scadparser/scad_ast.py --- solid/py_scadparser/scad_ast.py | 48 ---- solid/py_scadparser/scad_parser.py | 445 +++++++++++++++-------------- solid/py_scadparser/scad_tokens.py | 13 +- 3 files changed, 229 insertions(+), 277 deletions(-) delete mode 100644 solid/py_scadparser/scad_ast.py diff --git a/solid/py_scadparser/scad_ast.py b/solid/py_scadparser/scad_ast.py deleted file mode 100644 index 9bfc1aa4..00000000 --- a/solid/py_scadparser/scad_ast.py +++ /dev/null @@ -1,48 +0,0 @@ -from enum import Enum - -class ScadTypes(Enum): - GLOBAL_VAR = 0 - MODULE = 1 - FUNCTION = 2 - USE = 3 - INCLUDE = 4 - PARAMETER = 5 - -class ScadObject: - def __init__(self, scadType): - self.scadType = scadType - - def getType(self): - return self.scadType - -class ScadGlobalVar(ScadObject): - def __init__(self, name): - super().__init__(ScadTypes.GLOBAL_VAR) - self.name = name - -class ScadCallable(ScadObject): - def __init__(self, name, parameters, scadType): - super().__init__(scadType) - self.name = name - self.parameters = parameters - - def __repr__(self): - return f'{self.name} ({self.parameters})' - -class ScadModule(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.MODULE) - -class ScadFunction(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.FUNCTION) - -class ScadParameter(ScadObject): - def __init__(self, name, optional=False): - super().__init__(ScadTypes.PARAMETER) - self.name = name - self.optional = optional - - def __repr__(self): - return self.name + "=None" if self.optional else self.name - diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index 24e193cc..d97b98cd 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -1,249 +1,243 @@ +from enum import Enum + from ply import lex, yacc #workaround relative imports.... make this module runable as script if __name__ == "__main__": - from scad_ast import * from scad_tokens import * else: - from .scad_ast import * from .scad_tokens import * -precedence = ( - ('nonassoc', 'NO_ELSE'), - ('nonassoc', 'ELSE'), - ) - -def p_input(p): - """input :""" - p[0] = [] - -def p_input_use(p): - """input : input USE FILENAME - | input INCLUDE FILENAME""" - p[0] = p[1] +class ScadTypes(Enum): + GLOBAL_VAR = 0 + MODULE = 1 + FUNCTION = 2 + USE = 3 + INCLUDE = 4 + PARAMETER = 5 + +class ScadObject: + def __init__(self, scadType): + self.scadType = scadType + + def getType(self): + return self.scadType + +class ScadUse(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.USE) + self.filename = filename + +class ScadInclude(ScadObject): + def __init__(self, filename): + super().__init__(ScadTypes.INCLUDE) + self.filename = filename + +class ScadGlobalVar(ScadObject): + def __init__(self, name): + super().__init__(ScadTypes.GLOBAL_VAR) + self.name = name + +class ScadCallable(ScadObject): + def __init__(self, name, parameters, scadType): + super().__init__(scadType) + self.name = name + self.parameters = parameters + + def __repr__(self): + return f'{self.name} ({self.parameters})' + +class ScadModule(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.MODULE) + +class ScadFunction(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.FUNCTION) + +class ScadParameter(ScadObject): + def __init__(self, name, optional=False): + super().__init__(ScadTypes.PARAMETER) + self.name = name + self.optional = optional + + def __repr__(self): + return self.name + "=..." if self.optional else self.name -def p_input_statement(p): - """input : input statement""" +precedence = ( + ('nonassoc', "THEN"), + ('nonassoc', "ELSE"), + ('nonassoc', "?"), + ('nonassoc', ":"), + ('nonassoc', "[", "]", "(", ")", "{", "}"), + + ('nonassoc', '='), + ('left', "AND", "OR"), + ('nonassoc', "EQUAL", "NOT_EQUAL", "GREATER_OR_EQUAL", "LESS_OR_EQUAL", ">", "<"), + ('left', "%"), + ('left', '+', '-'), + ('left', '*', '/'), + ('right', 'NEG', 'POS', 'BACKGROUND', 'NOT'), + ('right', '^'), + ) + +def p_statements(p): + '''statements : statements statement''' p[0] = p[1] if p[2] != None: p[0].append(p[2]) +def p_statements_empty(p): + '''statements : empty''' + p[0] = [] + +def p_empty(p): + 'empty : ' + def p_statement(p): - """statement : ';' - | '{' inner_input '}' - | module_instantiation - """ - p[0] = None - -def p_statement_assigment(p): - """statement : assignment""" - p[0] = p[1] + ''' statement : IF "(" expression ")" statement %prec THEN + | IF "(" expression ")" statement ELSE statement + | for_loop statement + | LET "(" assignment_list ")" statement %prec THEN + | "{" statements "}" + | "%" statement %prec BACKGROUND + | "*" statement %prec BACKGROUND + | "!" statement %prec BACKGROUND + | call statement + | ";" + ''' + +def p_for_loop(p): + '''for_loop : FOR "(" parameter_list ")"''' + +def p_statement_use(p): + 'statement : USE FILENAME' + p[0] = ScadUse(p[2][1:len(p[2])-1]) + +def p_statement_include(p): + 'statement : INCLUDE FILENAME' + p[0] = ScadInclude(p[2][1:len(p[2])-1]) def p_statement_function(p): - """statement : MODULE ID '(' parameters optional_commas ')' statement - | FUNCTION ID '(' parameters optional_commas ')' '=' expr ';' - """ - if p[1] == 'module': - p[0] = ScadModule(p[2], p[4]) - elif p[1] == 'function': - p[0] = ScadFunction(p[2], p[4]) - else: - assert(False) + 'statement : function' + p[0] = p[1] -def p_inner_input(p): - """inner_input : - | inner_input statement - """ +def p_statement_module(p): + 'statement : module' + p[0] = p[1] -def p_assignment(p): - """assignment : ID '=' expr ';'""" +def p_statement_assignment(p): + 'statement : ID "=" expression ";"' p[0] = ScadGlobalVar(p[1]) -def p_module_instantiation(p): - """module_instantiation : '!' module_instantiation - | '#' module_instantiation - | '%' module_instantiation - | '*' module_instantiation - | single_module_instantiation child_statement - | ifelse_statement - """ - -def p_ifelse_statement(p): - """ifelse_statement : if_statement %prec NO_ELSE - | if_statement ELSE child_statement - """ - -def p_if_statement(p): - """if_statement : IF '(' expr ')' child_statement - """ - -def p_child_statements(p): - """child_statements : - | child_statements child_statement - | child_statements assignment - """ - -def p_child_statement(p): - """child_statement : ';' - | '{' child_statements '}' - | module_instantiation - """ - -def p_module_id(p): - """module_id : ID - | FOR - | LET - | ASSERT - | ECHO - | EACH - """ - -def p_single_module_instantiation(p): - """single_module_instantiation : module_id '(' arguments ')' - """ - -def p_expr(p): - """expr : logic_or - | FUNCTION '(' parameters optional_commas ')' expr %prec NO_ELSE - | logic_or '?' expr ':' expr - | LET '(' arguments ')' expr - | ASSERT '(' arguments ')' expr_or_empty - | ECHO '(' arguments ')' expr_or_empty - """ - -def p_logic_or(p): - """logic_or : logic_and - | logic_or OR logic_and - """ - -def p_logic_and(p): - """logic_and : equality - | logic_and AND equality - """ - -def p_equality(p): - """equality : comparison - | equality EQUAL comparison - | equality NOT_EQUAL comparison - """ - -def p_comparison(p): - """comparison : addition - | comparison '>' addition - | comparison GREATER_OR_EQUAL addition - | comparison '<' addition - | comparison LESS_OR_EQUAL addition - """ - -def p_addition(p): - """addition : multiplication - | addition '+' multiplication - | addition '-' multiplication - """ - -def p_multiplication(p): - """multiplication : unary - | multiplication '*' unary - | multiplication '/' unary - | multiplication '%' unary - """ - -def p_unary(p): - """unary : exponent - | '+' unary - | '-' unary - | '!' unary - """ - -def p_exponent(p): - """exponent : call - | call '^' unary - """ +def p_expression(p): + '''expression : ID + | expression "." ID + | "-" expression %prec NEG + | "+" expression %prec POS + | "!" expression %prec NOT + | expression "?" expression ":" expression + | expression "%" expression + | expression "+" expression + | expression "-" expression + | expression "/" expression + | expression "*" expression + | expression "^" expression + | expression "<" expression + | expression ">" expression + | expression EQUAL expression + | expression NOT_EQUAL expression + | expression GREATER_OR_EQUAL expression + | expression LESS_OR_EQUAL expression + | expression AND expression + | expression OR expression + | LET "(" assignment_list ")" expression %prec THEN + | EACH expression %prec THEN + | "[" expression ":" expression "]" + | "[" expression ":" expression ":" expression "]" + | "[" for_loop expression "]" + | for_loop expression %prec THEN + | IF "(" expression ")" expression %prec THEN + | IF "(" expression ")" expression ELSE expression + | "(" expression ")" + | call + | expression "[" expression "]" + | tuple + | STRING + | NUMBER''' + +def p_assignment_list(p): + '''assignment_list : ID "=" expression + | assignment_list "," ID "=" expression + ''' def p_call(p): - """call : primary - | call '(' arguments ')' - | call '[' expr ']' - | call '.' ID - """ - -def p_primary(p): - """primary : TRUE - | FALSE - | UNDEF - | NUMBER - | STRING - | ID - | '(' expr ')' - | '[' expr ':' expr ']' - | '[' expr ':' expr ':' expr ']' - | '[' optional_commas ']' - | '[' vector_expr optional_commas ']' - """ - -def p_expr_or_empty(p): - """expr_or_empty : - | expr - """ - -def p_list_comprehension_elements(p): - """list_comprehension_elements : LET '(' arguments ')' list_comprehension_elements_p - | EACH list_comprehension_elements_or_expr - | FOR '(' arguments ')' list_comprehension_elements_or_expr - | FOR '(' arguments ';' expr ';' arguments ')' list_comprehension_elements_or_expr - | IF '(' expr ')' list_comprehension_elements_or_expr %prec NO_ELSE - | IF '(' expr ')' list_comprehension_elements_or_expr ELSE list_comprehension_elements_or_expr - """ - -def p_list_comprehension_elements_p(p): - """list_comprehension_elements_p : list_comprehension_elements - | '(' list_comprehension_elements ')' - """ - -def p_list_comprehension_elements_or_expr(p): - """list_comprehension_elements_or_expr : list_comprehension_elements_p - | expr - """ - -def p_optional_commas(p): - """optional_commas : - | ',' optional_commas - """ - -def p_vector_expr(p): - """vector_expr : expr - | list_comprehension_elements - | vector_expr ',' optional_commas list_comprehension_elements_or_expr - """ - -def p_parameters(p): - """parameters : - | parameter - | parameters ',' optional_commas parameter - """ - if len(p) == 1: - p[0] = [] - elif len(p) == 2: - p[0] = [p[1]] + ''' call : ID "(" call_parameter_list ")" + | ID "(" ")"''' + +def p_tuple(p): + ''' tuple : "[" opt_expression_list "]" + ''' + +def p_opt_expression_list(p): + '''opt_expression_list : expression_list + | expression_list "," + | empty''' +def p_expression_list(p): + ''' expression_list : expression_list "," expression + | expression + ''' + +def p_call_parameter_list(p): + '''call_parameter_list : call_parameter_list "," call_parameter + | call_parameter''' + +def p_call_parameter(p): + '''call_parameter : expression + | ID "=" expression''' + +def p_opt_parameter_list(p): + '''opt_parameter_list : parameter_list + | parameter_list "," + | empty + ''' + if p[1] != None: + p[0] = p[1] else: - p[0] = p[1] + [p[4]] + p[0] = [] + +def p_parameter_list(p): + '''parameter_list : parameter_list "," parameter + | parameter''' + if len(p) > 2: + p[0] = p[1] + [p[3]] + else: + p[0] = [p[1]] def p_parameter(p): - """parameter : ID - | ID '=' expr - """ + '''parameter : ID + | ID "=" expression''' p[0] = ScadParameter(p[1], len(p) == 4) -def p_arguments(p): - """arguments : - | argument - | arguments ',' optional_commas argument - """ +def p_function(p): + '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression + ''' -def p_argument(p): - """argument : expr - | ID '=' expr - """ + params = None + if p[4] != ")": + params = p[4] + + p[0] = ScadFunction(p[2], params) + +def p_module(p): + '''module : MODULE ID "(" opt_parameter_list ")" statement + ''' + + params = None + if p[4] != ")": + params = p[4] + + p[0] = ScadModule(p[2], params) def p_error(p): print(f'py_scadparser: Syntax error: {p.lexer.filename}({p.lineno}) {p.type} - {p.value}') @@ -268,12 +262,20 @@ def parseFile(scadFile): for i in parser.parse(f.read(), lexer=lexer): appendObject[i.getType()](i) - return modules, functions, globalVars + return uses, includes, modules, functions, globalVars def parseFileAndPrintGlobals(scadFile): print(f'======{scadFile}======') - modules, functions, globalVars = parseFile(scadFile) + uses, includes, modules, functions, globalVars = parseFile(scadFile) + + print("Uses:") + for u in uses: + print(f' {u.filename}') + + print("Includes:") + for i in includes: + print(f' {i.filename}') print("Modules:") for m in modules: @@ -283,7 +285,7 @@ def parseFileAndPrintGlobals(scadFile): for m in functions: print(f' {m}') - print("Global Variables:") + print("Global Vars:") for m in globalVars: print(f' {m.name}') @@ -298,6 +300,7 @@ def parseFileAndPrintGlobals(scadFile): for i in files: if quiete: + print(i) parseFile(i) else: parseFileAndPrintGlobals(i) diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index 1bd9d85d..f79bcc03 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -6,7 +6,7 @@ "+", "-", "*", "/", "^", "?", ":", "[", "]", "{", "}", "(", ")", - "%", "#" + "%", ] reserved = { @@ -16,14 +16,9 @@ 'function' : 'FUNCTION', 'if' : 'IF', 'else' : 'ELSE', - 'let' : 'LET', - 'assert' : 'ASSERT', - 'echo' : 'ECHO', 'for' : 'FOR', + 'let' : 'LET', 'each' : 'EACH', - 'true' : 'TRUE', - 'false' : 'FALSE', - 'undef' : 'UNDEF', } tokens = [ @@ -58,6 +53,8 @@ t_FILENAME = r'<[a-zA-Z_0-9/\\\.-]*>' +t_ignore = "#$" + def t_eat_escaped_quotes(t): r"\\\"" pass @@ -77,7 +74,7 @@ def t_whitespace(t): t.lexer.lineno += t.value.count("\n") def t_ID(t): - r'[\$]?[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' + r'[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' t.type = reserved.get(t.value,'ID') return t From 2c052dbf9117c70473c0a2358e0d730310dd52b4 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Tue, 25 May 2021 17:45:31 +0200 Subject: [PATCH 79/90] improved "home brewed" parser version --- solid/py_scadparser/parsetab.py | 127 +++++++++++++++++++++ solid/py_scadparser/scad_ast.py | 48 ++++++++ solid/py_scadparser/scad_parser.py | 174 ++++++++++++----------------- solid/py_scadparser/scad_tokens.py | 14 ++- 4 files changed, 257 insertions(+), 106 deletions(-) create mode 100644 solid/py_scadparser/parsetab.py create mode 100644 solid/py_scadparser/scad_ast.py diff --git a/solid/py_scadparser/parsetab.py b/solid/py_scadparser/parsetab.py new file mode 100644 index 00000000..3ab03530 --- /dev/null +++ b/solid/py_scadparser/parsetab.py @@ -0,0 +1,127 @@ + +# parsetab.py +# This file is automatically generated. Do not edit. +# pylint: disable=W,C,R +_tabversion = '3.10' + +_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 assert_or_echo : ASSERT "(" opt_call_parameter_list ")"\n | ECHO "(" opt_call_parameter_list ")"\n constants : STRING\n | TRUE\n | FALSE\n | NUMBERfor_or_if : for_loop expression %prec THEN\n | IF "(" expression ")" expression %prec THEN\n | IF "(" expression ")" expression ELSE expression\n | tuple\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,62,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,210,211,212,],[-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,-63,-64,-65,42,-68,-69,-42,42,42,42,42,42,42,-55,-56,-57,-58,-62,-10,-74,42,42,4,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,-67,42,-24,-25,-26,-49,-59,42,42,4,42,4,42,-77,42,4,-23,-73,-19,42,42,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,42,-75,42,-7,-8,-76,-9,4,42,-44,4,-46,42,-52,42,42,-53,-54,42,42,-97,-60,-5,-27,42,-50,-47,-48,-96,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,62,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,210,211,212,],[-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,-63,-64,-65,57,-68,-69,-42,57,57,57,57,57,57,-55,-56,-57,-58,-62,-10,-74,57,57,6,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,-67,57,-24,-25,-26,-49,-59,57,57,6,57,6,57,-77,57,6,-23,-73,-19,57,57,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,57,-75,57,-7,-8,-76,-9,6,57,-44,6,-46,57,-52,57,57,-53,-54,57,57,-97,-60,-5,-27,57,-50,-47,-48,-96,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,62,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,210,211,212,],[-3,7,-2,-1,7,-3,7,7,7,7,7,-18,-21,-22,60,-6,60,60,7,-11,-12,-13,-14,-15,-16,-17,60,60,60,-63,-64,-65,60,-68,-69,-42,60,60,60,60,60,60,-55,-56,-57,-58,-62,-10,-74,60,60,7,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,-67,60,-24,-25,-26,-49,-59,60,60,7,60,7,60,-77,60,7,-23,-73,-19,60,60,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,60,-75,60,-7,-8,-76,-9,7,60,-44,7,-46,60,-52,60,60,-53,-54,60,60,-97,-60,-5,-27,60,-50,-47,-48,-96,60,-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,62,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,210,211,212,],[-3,8,-2,-1,8,-3,8,8,8,8,8,-18,-21,-22,61,-6,61,61,8,-11,-12,-13,-14,-15,-16,-17,61,61,61,-63,-64,-65,61,-68,-69,-42,61,61,61,61,61,61,-55,-56,-57,-58,-62,-10,-74,61,61,8,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,-67,61,-24,-25,-26,-49,-59,61,61,8,61,8,61,-77,61,8,-23,-73,-19,61,61,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,61,-75,61,-7,-8,-76,-9,8,61,-44,8,-46,61,-52,61,61,-53,-54,61,61,-97,-60,-5,-27,61,-50,-47,-48,-96,61,-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,62,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,210,211,212,],[-3,9,-2,-1,9,-3,9,9,9,9,9,-18,-21,-22,-6,9,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,9,-67,-24,-25,-26,-49,-59,9,9,9,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,9,-44,9,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,209,210,211,212,],[-3,10,-2,-1,10,-3,10,10,10,10,10,-18,-21,-22,-6,10,-11,-12,-13,-14,-15,-16,-17,91,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,91,-42,-10,91,-74,91,10,91,91,-24,-25,-26,91,91,10,10,10,-23,-73,-19,91,-70,-45,-4,-43,91,91,-28,91,91,-31,-32,-33,91,91,91,91,91,91,91,91,91,-75,-7,91,-8,91,-9,91,91,10,-44,10,-46,91,-52,91,-53,-54,-97,91,-5,91,-50,91,91,91,91,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,62,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,209,210,211,212,],[-3,11,-2,-1,11,-3,11,11,11,11,11,-18,-21,-22,-6,11,-11,-12,-13,-14,-15,-16,-17,95,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,95,-42,-10,95,-74,95,11,95,95,-24,-25,-26,95,95,11,11,11,-23,-73,-19,95,-70,-45,-4,-43,95,95,95,95,95,-31,-32,-33,95,95,95,95,95,95,95,95,95,-75,-7,95,-8,95,-9,95,95,11,-44,11,-46,95,-52,95,-53,-54,-97,95,-5,95,-50,95,95,95,95,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,62,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,210,211,212,],[-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,-63,-64,-65,55,-68,-69,-42,55,55,55,55,55,55,-55,-56,-57,-58,-62,-10,-74,55,55,12,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,-67,55,-24,-25,-26,-49,-59,55,55,12,55,12,55,-77,55,12,-23,-73,-19,55,55,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,55,-75,55,-7,-8,-76,-9,12,55,-44,12,-46,55,-52,55,55,-53,-54,55,55,-97,-60,-5,-27,55,-50,-47,-48,-96,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,62,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,210,211,212,],[-3,13,-2,-1,13,-3,13,13,13,13,13,-18,-21,-22,-6,13,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,13,-67,-24,-25,-26,-49,-59,13,13,13,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,13,-44,13,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-3,15,-2,-1,15,-3,15,15,15,15,15,-18,-21,-22,-6,15,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,15,-67,-24,-25,-26,-49,-59,15,15,15,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,15,-44,15,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-3,16,-2,-1,16,-3,16,16,16,16,16,-18,-21,-22,-6,16,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,16,-67,-24,-25,-26,-49,-59,16,16,16,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,16,-44,16,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-3,17,-2,-1,17,-3,17,17,17,17,17,-18,-21,-22,-6,17,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,128,-74,131,-93,-94,17,-67,-24,-25,-26,-49,-59,17,17,17,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,195,-92,-95,17,-44,17,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-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,-63,-64,-65,51,-68,-69,-42,51,51,51,51,51,51,-55,-56,-57,-58,-62,-10,-74,81,81,51,73,20,143,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,-67,51,-24,-25,-26,81,68,-49,-59,73,73,20,169,51,20,73,-77,51,20,-23,-73,-19,51,81,51,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,51,-75,51,-7,-8,-76,-9,81,20,51,-44,20,-46,51,-52,51,51,-53,-54,51,81,51,-97,-60,-5,-27,51,-50,-47,-48,-96,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,62,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,210,211,212,],[-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,-63,-64,-65,21,-68,-69,-42,21,21,21,21,21,21,-55,-56,-57,-58,-62,-10,-74,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,-67,21,-24,-25,-26,-49,-59,21,21,21,21,21,21,-77,21,21,-23,-73,-19,21,21,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,21,-75,21,-7,-8,-76,-9,21,21,-44,21,-46,21,-52,21,21,-53,-54,21,21,-97,-60,-5,-27,21,-50,-47,-48,-96,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,62,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,210,211,212,],[-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,-63,-64,-65,56,-68,-69,-42,56,56,56,56,56,56,-55,-56,-57,-58,-62,-10,-74,56,56,22,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,-67,56,-24,-25,-26,-49,-59,56,56,22,56,22,56,-77,56,22,-23,-73,-19,56,56,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,56,-75,56,-7,-8,-76,-9,22,56,-44,22,-46,56,-52,56,56,-53,-54,56,56,-97,-60,-5,-27,56,-50,-47,-48,-96,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,62,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,210,211,212,],[-3,23,-2,-1,23,-3,23,23,23,23,23,-18,-21,-22,-6,23,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,23,-67,-24,-25,-26,-49,-59,23,23,23,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,23,-44,23,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,211,212,],[-3,0,-2,-1,-18,-21,-22,-6,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-67,-24,-25,-26,-49,-59,-23,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,-44,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-61,-51,]),'}':([2,3,9,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,62,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,211,212,],[-2,-1,-3,-18,-21,-22,-6,75,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-67,-24,-25,-26,-49,-59,-23,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,-44,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,],[24,26,27,28,38,39,43,43,43,43,43,82,83,84,43,86,-63,-64,-65,43,-68,-69,-42,43,43,43,43,114,115,43,43,118,119,-55,-56,-57,-58,-62,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,-77,43,-19,43,43,86,-70,-45,-43,86,86,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,43,86,-75,43,86,-76,86,86,86,43,-44,-46,43,86,-52,86,43,43,-53,-54,43,43,86,86,43,-50,86,86,86,86,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,62,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,211,212,],[-18,-21,-22,-6,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-67,-24,-25,-26,-49,-59,-23,-70,-45,184,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,-44,-46,-52,-53,-54,-97,208,-5,-27,-50,-47,-48,-96,-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,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,],[53,53,53,53,53,53,93,-63,-64,-65,53,-68,-69,-42,53,53,53,53,53,53,-55,-56,-57,-58,-62,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,-77,53,-19,53,53,93,-70,-45,-43,93,93,-28,-29,-30,-31,-32,-33,93,93,93,93,93,93,93,93,53,93,-75,53,93,-76,93,93,93,53,-44,-46,53,93,-52,93,53,53,-53,-54,53,53,93,93,53,-50,93,93,93,93,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,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,],[54,54,54,54,54,54,92,-63,-64,-65,54,-68,-69,-42,54,54,54,54,54,54,-55,-56,-57,-58,-62,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,-77,54,-19,54,54,92,-70,-45,-43,92,92,-28,-29,-30,-31,-32,-33,92,92,92,92,92,92,92,92,54,92,-75,54,92,-76,92,92,92,54,-44,-46,54,92,-52,92,54,54,-53,-54,54,54,92,92,54,-50,92,92,92,92,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,208,210,],[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,-77,58,-19,58,58,58,58,-76,58,58,58,58,-53,-54,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,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,],[52,52,52,52,52,52,89,-63,-64,-65,52,-68,-69,-42,52,52,52,52,52,52,-55,-56,-57,-58,-62,89,-42,89,52,89,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,-67,89,52,-24,-25,-26,-49,-59,52,52,52,52,-77,52,-19,52,52,89,-70,-45,-43,89,89,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,52,-59,-75,52,89,-76,89,89,89,52,-44,-46,52,89,-52,89,52,52,-53,-54,52,52,-60,-27,52,-50,-47,-48,89,89,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,208,210,],[62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,-77,62,-19,62,62,62,62,-76,62,62,62,62,-53,-54,62,62,62,62,-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,208,210,],[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,-77,63,-19,63,63,63,63,-76,63,63,63,63,-53,-54,63,63,63,63,-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,208,210,],[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,-77,64,-19,64,64,64,64,-76,64,64,64,64,-53,-54,64,64,64,64,-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,208,210,],[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,-77,65,-19,65,65,65,65,-76,65,65,65,65,-53,-54,65,65,65,65,-20,]),')':([27,28,38,44,45,46,47,48,49,50,51,62,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,211,212,],[-83,-83,78,87,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,120,123,-84,-86,-87,-42,127,129,130,-93,-94,-3,-3,139,141,-67,-24,-25,-26,-3,-49,-59,-83,-83,-77,179,-89,-91,181,182,-70,183,-45,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,190,191,192,193,-71,-85,-76,-88,-92,-95,-90,-44,-46,-52,-53,-54,-60,-27,-50,-47,-48,-72,210,-61,-51,]),'.':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[88,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,88,-42,88,88,-67,88,-24,-25,-26,-49,-59,88,-70,-45,-43,88,88,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,88,88,88,88,-44,-46,88,-52,88,-53,-54,-60,-27,-50,-47,-48,88,88,88,-61,-51,]),'?':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[90,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,90,-42,90,90,90,90,-24,-25,-26,90,90,90,-70,-45,-43,90,90,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,90,-75,90,90,90,90,-44,-46,90,-52,90,-53,-54,90,-27,-50,-47,90,90,90,90,90,-51,]),'/':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[94,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,94,-42,94,94,94,94,-24,-25,-26,94,94,94,-70,-45,-43,94,94,94,94,94,-31,-32,-33,94,94,94,94,94,94,94,94,94,-75,94,94,94,94,-44,-46,94,-52,94,-53,-54,94,94,-50,94,94,94,94,94,94,-51,]),'^':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[96,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,96,-42,96,96,96,96,-24,-25,-26,96,96,96,-70,-45,-43,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,-75,96,96,96,96,-44,-46,96,-52,96,-53,-54,96,96,-50,96,96,96,96,96,96,-51,]),'<':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[97,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,97,-42,97,97,97,97,-24,-25,-26,97,97,97,-70,-45,-43,97,97,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,97,97,97,-75,97,97,97,97,-44,-46,97,-52,97,-53,-54,97,97,-50,97,97,97,97,97,97,-51,]),'>':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[98,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,98,-42,98,98,98,98,-24,-25,-26,98,98,98,-70,-45,-43,98,98,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,98,98,98,-75,98,98,98,98,-44,-46,98,-52,98,-53,-54,98,98,-50,98,98,98,98,98,98,-51,]),'EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[99,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,99,-42,99,99,99,99,-24,-25,-26,99,99,99,-70,-45,-43,99,99,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,99,99,99,-75,99,99,99,99,-44,-46,99,-52,99,-53,-54,99,99,-50,99,99,99,99,99,99,-51,]),'NOT_EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[100,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,100,-42,100,100,100,100,-24,-25,-26,100,100,100,-70,-45,-43,100,100,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,100,100,100,-75,100,100,100,100,-44,-46,100,-52,100,-53,-54,100,100,-50,100,100,100,100,100,100,-51,]),'GREATER_OR_EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[101,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,101,-42,101,101,101,101,-24,-25,-26,101,101,101,-70,-45,-43,101,101,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,101,101,101,-75,101,101,101,101,-44,-46,101,-52,101,-53,-54,101,101,-50,101,101,101,101,101,101,-51,]),'LESS_OR_EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[102,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,102,-42,102,102,102,102,-24,-25,-26,102,102,102,-70,-45,-43,102,102,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,102,102,102,-75,102,102,102,102,-44,-46,102,-52,102,-53,-54,102,102,-50,102,102,102,102,102,102,-51,]),'AND':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[103,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,103,-42,103,103,103,103,-24,-25,-26,103,103,103,-70,-45,-43,103,103,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,103,-75,103,103,103,103,-44,-46,103,-52,103,-53,-54,103,103,-50,103,103,103,103,103,103,-51,]),'OR':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[104,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,104,-42,104,104,104,104,-24,-25,-26,104,104,104,-70,-45,-43,104,104,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,104,-75,104,104,104,104,-44,-46,104,-52,104,-53,-54,104,104,-50,104,104,104,104,104,104,-51,]),',':([45,46,47,48,49,50,51,62,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,211,212,],[-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,121,125,-86,-87,-42,125,125,-93,-94,-67,-82,125,-24,-25,-26,-49,-59,173,-77,173,125,-70,125,-45,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,173,121,-71,-85,-76,-88,-92,-95,173,-44,-46,-52,-81,-53,-54,-60,-27,-50,-47,-48,-72,125,-61,-51,]),':':([45,46,47,48,49,50,51,62,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,211,212,],[-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-67,160,-24,-25,-26,-49,-59,-70,-45,-43,186,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,-44,-46,201,-52,-53,-54,-60,-27,-50,-47,-48,-61,-51,]),']':([45,46,47,48,49,50,51,52,62,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,209,211,212,],[-63,-64,-65,-66,-68,-69,-42,-3,-55,-56,-57,-58,-62,-67,-82,162,-78,-80,-24,-25,-26,-49,-59,-77,-70,-45,-43,185,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,-79,-76,-44,-46,202,-52,-81,-53,-54,-60,-27,-50,-47,-48,212,-61,-51,]),} + +_lr_action = {} +for _k, _v in _lr_action_items.items(): + for _x,_y in zip(_v[0],_v[1]): + if not _x in _lr_action: _lr_action[_x] = {} + _lr_action[_x][_k] = _y +del _lr_action_items + +_lr_goto_items = {'statements':([0,9,],[1,29,]),'empty':([0,9,52,82,83,114,],[2,2,110,136,136,136,]),'statement':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[3,25,30,31,32,33,34,3,142,168,171,175,197,199,]),'for_loop':([1,5,10,11,12,13,14,24,27,28,29,37,38,43,48,52,53,54,55,58,59,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,120,122,123,124,126,127,131,133,160,163,181,182,184,186,190,191,194,196,201,208,],[5,5,5,5,5,5,5,59,59,59,5,59,59,59,59,107,59,59,59,59,59,59,59,5,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,5,59,5,59,59,5,59,59,59,59,5,59,5,59,59,59,59,59,59,59,]),'call':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[14,14,14,14,14,14,14,14,14,14,14,14,14,14,]),'function':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,]),'module':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[19,19,19,19,19,19,19,19,19,19,19,19,19,19,]),'expression':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[44,72,72,76,72,85,105,106,111,112,113,116,117,138,72,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,72,72,170,72,174,176,178,187,189,198,200,203,204,205,207,209,211,]),'access_expr':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,]),'logic_expr':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,]),'list_stuff':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,]),'assert_or_echo':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,]),'constants':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,]),'for_or_if':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,]),'tuple':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[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,66,66,66,66,66,66,66,66,66,66,66,66,66,]),'assignment_list':([26,115,],[67,165,]),'opt_call_parameter_list':([27,28,118,119,],[69,74,166,167,]),'call_parameter_list':([27,28,38,86,118,119,],[70,70,77,140,70,70,]),'call_parameter':([27,28,38,86,118,119,124,],[71,71,71,71,71,71,172,]),'parameter_list':([39,82,83,114,195,],[79,135,135,135,206,]),'parameter':([39,82,83,114,132,180,195,],[80,80,80,80,177,177,80,]),'opt_expression_list':([52,],[108,]),'expression_list':([52,],[109,]),'commas':([70,77,79,109,135,140,206,],[124,124,132,163,180,124,132,]),'opt_parameter_list':([82,83,114,],[134,137,164,]),} + +_lr_goto = {} +for _k, _v in _lr_goto_items.items(): + for _x, _y in zip(_v[0], _v[1]): + if not _x in _lr_goto: _lr_goto[_x] = {} + _lr_goto[_x][_k] = _y +del _lr_goto_items +_lr_productions = [ + ("S' -> statements","S'",1,None,None,None), + ('statements -> statements statement','statements',2,'p_statements','scad_parser.py',32), + ('statements -> empty','statements',1,'p_statements_empty','scad_parser.py',38), + ('empty -> ','empty',0,'p_empty','scad_parser.py',42), + ('statement -> IF ( expression ) statement','statement',5,'p_statement','scad_parser.py',45), + ('statement -> IF ( expression ) statement ELSE statement','statement',7,'p_statement','scad_parser.py',46), + ('statement -> for_loop statement','statement',2,'p_statement','scad_parser.py',47), + ('statement -> LET ( assignment_list ) statement','statement',5,'p_statement','scad_parser.py',48), + ('statement -> ASSERT ( opt_call_parameter_list ) statement','statement',5,'p_statement','scad_parser.py',49), + ('statement -> ECHO ( opt_call_parameter_list ) statement','statement',5,'p_statement','scad_parser.py',50), + ('statement -> { statements }','statement',3,'p_statement','scad_parser.py',51), + ('statement -> % statement','statement',2,'p_statement','scad_parser.py',52), + ('statement -> * statement','statement',2,'p_statement','scad_parser.py',53), + ('statement -> ! statement','statement',2,'p_statement','scad_parser.py',54), + ('statement -> # statement','statement',2,'p_statement','scad_parser.py',55), + ('statement -> call statement','statement',2,'p_statement','scad_parser.py',56), + ('statement -> USE FILENAME','statement',2,'p_statement','scad_parser.py',57), + ('statement -> INCLUDE FILENAME','statement',2,'p_statement','scad_parser.py',58), + ('statement -> ;','statement',1,'p_statement','scad_parser.py',59), + ('for_loop -> FOR ( parameter_list )','for_loop',4,'p_for_loop','scad_parser.py',62), + ('for_loop -> FOR ( parameter_list ; expression ; parameter_list )','for_loop',8,'p_for_loop','scad_parser.py',63), + ('statement -> function','statement',1,'p_statement_function','scad_parser.py',66), + ('statement -> module','statement',1,'p_statement_module','scad_parser.py',70), + ('statement -> ID = expression ;','statement',4,'p_statement_assignment','scad_parser.py',74), + ('logic_expr -> - expression','logic_expr',2,'p_logic_expr','scad_parser.py',78), + ('logic_expr -> + expression','logic_expr',2,'p_logic_expr','scad_parser.py',79), + ('logic_expr -> ! expression','logic_expr',2,'p_logic_expr','scad_parser.py',80), + ('logic_expr -> expression ? expression : expression','logic_expr',5,'p_logic_expr','scad_parser.py',81), + ('logic_expr -> expression % expression','logic_expr',3,'p_logic_expr','scad_parser.py',82), + ('logic_expr -> expression + expression','logic_expr',3,'p_logic_expr','scad_parser.py',83), + ('logic_expr -> expression - expression','logic_expr',3,'p_logic_expr','scad_parser.py',84), + ('logic_expr -> expression / expression','logic_expr',3,'p_logic_expr','scad_parser.py',85), + ('logic_expr -> expression * expression','logic_expr',3,'p_logic_expr','scad_parser.py',86), + ('logic_expr -> expression ^ expression','logic_expr',3,'p_logic_expr','scad_parser.py',87), + ('logic_expr -> expression < expression','logic_expr',3,'p_logic_expr','scad_parser.py',88), + ('logic_expr -> expression > expression','logic_expr',3,'p_logic_expr','scad_parser.py',89), + ('logic_expr -> expression EQUAL expression','logic_expr',3,'p_logic_expr','scad_parser.py',90), + ('logic_expr -> expression NOT_EQUAL expression','logic_expr',3,'p_logic_expr','scad_parser.py',91), + ('logic_expr -> expression GREATER_OR_EQUAL expression','logic_expr',3,'p_logic_expr','scad_parser.py',92), + ('logic_expr -> expression LESS_OR_EQUAL expression','logic_expr',3,'p_logic_expr','scad_parser.py',93), + ('logic_expr -> expression AND expression','logic_expr',3,'p_logic_expr','scad_parser.py',94), + ('logic_expr -> expression OR expression','logic_expr',3,'p_logic_expr','scad_parser.py',95), + ('access_expr -> ID','access_expr',1,'p_access_expr','scad_parser.py',99), + ('access_expr -> expression . ID','access_expr',3,'p_access_expr','scad_parser.py',100), + ('access_expr -> expression ( call_parameter_list )','access_expr',4,'p_access_expr','scad_parser.py',101), + ('access_expr -> expression ( )','access_expr',3,'p_access_expr','scad_parser.py',102), + ('access_expr -> expression [ expression ]','access_expr',4,'p_access_expr','scad_parser.py',103), + ('list_stuff -> FUNCTION ( opt_parameter_list ) expression','list_stuff',5,'p_list_stuff','scad_parser.py',107), + ('list_stuff -> LET ( assignment_list ) expression','list_stuff',5,'p_list_stuff','scad_parser.py',108), + ('list_stuff -> EACH expression','list_stuff',2,'p_list_stuff','scad_parser.py',109), + ('list_stuff -> [ expression : expression ]','list_stuff',5,'p_list_stuff','scad_parser.py',110), + ('list_stuff -> [ expression : expression : expression ]','list_stuff',7,'p_list_stuff','scad_parser.py',111), + ('list_stuff -> [ for_loop expression ]','list_stuff',4,'p_list_stuff','scad_parser.py',112), + ('assert_or_echo -> ASSERT ( opt_call_parameter_list )','assert_or_echo',4,'p_assert_or_echo','scad_parser.py',116), + ('assert_or_echo -> ECHO ( opt_call_parameter_list )','assert_or_echo',4,'p_assert_or_echo','scad_parser.py',117), + ('constants -> STRING','constants',1,'p_constants','scad_parser.py',120), + ('constants -> TRUE','constants',1,'p_constants','scad_parser.py',121), + ('constants -> FALSE','constants',1,'p_constants','scad_parser.py',122), + ('constants -> NUMBER','constants',1,'p_constants','scad_parser.py',123), + ('for_or_if -> for_loop expression','for_or_if',2,'p_for_or_if','scad_parser.py',126), + ('for_or_if -> IF ( expression ) expression','for_or_if',5,'p_for_or_if','scad_parser.py',127), + ('for_or_if -> IF ( expression ) expression ELSE expression','for_or_if',7,'p_for_or_if','scad_parser.py',128), + ('for_or_if -> tuple','for_or_if',1,'p_for_or_if','scad_parser.py',129), + ('expression -> access_expr','expression',1,'p_expression','scad_parser.py',133), + ('expression -> logic_expr','expression',1,'p_expression','scad_parser.py',134), + ('expression -> list_stuff','expression',1,'p_expression','scad_parser.py',135), + ('expression -> assert_or_echo','expression',1,'p_expression','scad_parser.py',136), + ('expression -> assert_or_echo expression','expression',2,'p_expression','scad_parser.py',137), + ('expression -> constants','expression',1,'p_expression','scad_parser.py',138), + ('expression -> for_or_if','expression',1,'p_expression','scad_parser.py',139), + ('expression -> ( expression )','expression',3,'p_expression','scad_parser.py',140), + ('assignment_list -> ID = expression','assignment_list',3,'p_assignment_list','scad_parser.py',144), + ('assignment_list -> assignment_list , ID = expression','assignment_list',5,'p_assignment_list','scad_parser.py',145), + ('call -> ID ( call_parameter_list )','call',4,'p_call','scad_parser.py',149), + ('call -> ID ( )','call',3,'p_call','scad_parser.py',150), + ('tuple -> [ opt_expression_list ]','tuple',3,'p_tuple','scad_parser.py',153), + ('commas -> commas ,','commas',2,'p_commas','scad_parser.py',157), + ('commas -> ,','commas',1,'p_commas','scad_parser.py',158), + ('opt_expression_list -> expression_list','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',162), + ('opt_expression_list -> expression_list commas','opt_expression_list',2,'p_opt_expression_list','scad_parser.py',163), + ('opt_expression_list -> empty','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',164), + ('expression_list -> expression_list commas expression','expression_list',3,'p_expression_list','scad_parser.py',166), + ('expression_list -> expression','expression_list',1,'p_expression_list','scad_parser.py',167), + ('opt_call_parameter_list -> ','opt_call_parameter_list',0,'p_opt_call_parameter_list','scad_parser.py',171), + ('opt_call_parameter_list -> call_parameter_list','opt_call_parameter_list',1,'p_opt_call_parameter_list','scad_parser.py',172), + ('call_parameter_list -> call_parameter_list commas call_parameter','call_parameter_list',3,'p_call_parameter_list','scad_parser.py',175), + ('call_parameter_list -> call_parameter','call_parameter_list',1,'p_call_parameter_list','scad_parser.py',176), + ('call_parameter -> expression','call_parameter',1,'p_call_parameter','scad_parser.py',179), + ('call_parameter -> ID = expression','call_parameter',3,'p_call_parameter','scad_parser.py',180), + ('opt_parameter_list -> parameter_list','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',183), + ('opt_parameter_list -> parameter_list commas','opt_parameter_list',2,'p_opt_parameter_list','scad_parser.py',184), + ('opt_parameter_list -> empty','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',185), + ('parameter_list -> parameter_list commas parameter','parameter_list',3,'p_parameter_list','scad_parser.py',193), + ('parameter_list -> parameter','parameter_list',1,'p_parameter_list','scad_parser.py',194), + ('parameter -> ID','parameter',1,'p_parameter','scad_parser.py',201), + ('parameter -> ID = expression','parameter',3,'p_parameter','scad_parser.py',202), + ('function -> FUNCTION ID ( opt_parameter_list ) = expression','function',7,'p_function','scad_parser.py',206), + ('module -> MODULE ID ( opt_parameter_list ) statement','module',6,'p_module','scad_parser.py',215), +] diff --git a/solid/py_scadparser/scad_ast.py b/solid/py_scadparser/scad_ast.py new file mode 100644 index 00000000..9bfc1aa4 --- /dev/null +++ b/solid/py_scadparser/scad_ast.py @@ -0,0 +1,48 @@ +from enum import Enum + +class ScadTypes(Enum): + GLOBAL_VAR = 0 + MODULE = 1 + FUNCTION = 2 + USE = 3 + INCLUDE = 4 + PARAMETER = 5 + +class ScadObject: + def __init__(self, scadType): + self.scadType = scadType + + def getType(self): + return self.scadType + +class ScadGlobalVar(ScadObject): + def __init__(self, name): + super().__init__(ScadTypes.GLOBAL_VAR) + self.name = name + +class ScadCallable(ScadObject): + def __init__(self, name, parameters, scadType): + super().__init__(scadType) + self.name = name + self.parameters = parameters + + def __repr__(self): + return f'{self.name} ({self.parameters})' + +class ScadModule(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.MODULE) + +class ScadFunction(ScadCallable): + def __init__(self, name, parameters): + super().__init__(name, parameters, ScadTypes.FUNCTION) + +class ScadParameter(ScadObject): + def __init__(self, name, optional=False): + super().__init__(ScadTypes.PARAMETER) + self.name = name + self.optional = optional + + def __repr__(self): + return self.name + "=None" if self.optional else self.name + diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index d97b98cd..f95f50d4 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -1,84 +1,31 @@ -from enum import Enum - from ply import lex, yacc #workaround relative imports.... make this module runable as script if __name__ == "__main__": + from scad_ast import * from scad_tokens import * else: + from .scad_ast import * from .scad_tokens import * -class ScadTypes(Enum): - GLOBAL_VAR = 0 - MODULE = 1 - FUNCTION = 2 - USE = 3 - INCLUDE = 4 - PARAMETER = 5 - -class ScadObject: - def __init__(self, scadType): - self.scadType = scadType - - def getType(self): - return self.scadType - -class ScadUse(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.USE) - self.filename = filename - -class ScadInclude(ScadObject): - def __init__(self, filename): - super().__init__(ScadTypes.INCLUDE) - self.filename = filename - -class ScadGlobalVar(ScadObject): - def __init__(self, name): - super().__init__(ScadTypes.GLOBAL_VAR) - self.name = name - -class ScadCallable(ScadObject): - def __init__(self, name, parameters, scadType): - super().__init__(scadType) - self.name = name - self.parameters = parameters - - def __repr__(self): - return f'{self.name} ({self.parameters})' - -class ScadModule(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.MODULE) - -class ScadFunction(ScadCallable): - def __init__(self, name, parameters): - super().__init__(name, parameters, ScadTypes.FUNCTION) - -class ScadParameter(ScadObject): - def __init__(self, name, optional=False): - super().__init__(ScadTypes.PARAMETER) - self.name = name - self.optional = optional - - def __repr__(self): - return self.name + "=..." if self.optional else self.name - precedence = ( + ('nonassoc', 'ASSERT'), + ('nonassoc', 'ECHO'), ('nonassoc', "THEN"), ('nonassoc', "ELSE"), ('nonassoc', "?"), ('nonassoc', ":"), - ('nonassoc', "[", "]", "(", ")", "{", "}"), + ('nonassoc', "(", ")", "{", "}"), ('nonassoc', '='), ('left', "AND", "OR"), - ('nonassoc', "EQUAL", "NOT_EQUAL", "GREATER_OR_EQUAL", "LESS_OR_EQUAL", ">", "<"), - ('left', "%"), + ('left', "EQUAL", "NOT_EQUAL", "GREATER_OR_EQUAL", "LESS_OR_EQUAL", ">", "<"), ('left', '+', '-'), + ('left', "%"), ('left', '*', '/'), - ('right', 'NEG', 'POS', 'BACKGROUND', 'NOT'), ('right', '^'), + ('right', 'NEG', 'POS', 'BACKGROUND', 'NOT'), + ('left', "ACCESS"), ) def p_statements(p): @@ -99,24 +46,21 @@ def p_statement(p): | IF "(" expression ")" statement ELSE statement | for_loop statement | LET "(" assignment_list ")" statement %prec THEN + | ASSERT "(" opt_call_parameter_list ")" statement + | ECHO "(" opt_call_parameter_list ")" statement | "{" statements "}" | "%" statement %prec BACKGROUND | "*" statement %prec BACKGROUND | "!" statement %prec BACKGROUND + | "#" statement %prec BACKGROUND | call statement + | USE FILENAME + | INCLUDE FILENAME | ";" ''' - def p_for_loop(p): - '''for_loop : FOR "(" parameter_list ")"''' - -def p_statement_use(p): - 'statement : USE FILENAME' - p[0] = ScadUse(p[2][1:len(p[2])-1]) - -def p_statement_include(p): - 'statement : INCLUDE FILENAME' - p[0] = ScadInclude(p[2][1:len(p[2])-1]) + '''for_loop : FOR "(" parameter_list ")" + | FOR "(" parameter_list ";" expression ";" parameter_list ")"''' def p_statement_function(p): 'statement : function' @@ -130,10 +74,8 @@ def p_statement_assignment(p): 'statement : ID "=" expression ";"' p[0] = ScadGlobalVar(p[1]) -def p_expression(p): - '''expression : ID - | expression "." ID - | "-" expression %prec NEG +def p_logic_expr(p): + '''logic_expr : "-" expression %prec NEG | "+" expression %prec POS | "!" expression %prec NOT | expression "?" expression ":" expression @@ -151,20 +93,52 @@ def p_expression(p): | expression LESS_OR_EQUAL expression | expression AND expression | expression OR expression + ''' + +def p_access_expr(p): + '''access_expr : ID %prec ACCESS + | expression "." ID %prec ACCESS + | expression "(" call_parameter_list ")" %prec ACCESS + | expression "(" ")" %prec ACCESS + | expression "[" expression "]" %prec ACCESS + ''' + +def p_list_stuff(p): + '''list_stuff : FUNCTION "(" opt_parameter_list ")" expression | LET "(" assignment_list ")" expression %prec THEN | EACH expression %prec THEN | "[" expression ":" expression "]" | "[" expression ":" expression ":" expression "]" | "[" for_loop expression "]" - | for_loop expression %prec THEN + ''' + +def p_assert_or_echo(p): + '''assert_or_echo : ASSERT "(" opt_call_parameter_list ")" + | ECHO "(" opt_call_parameter_list ")" + ''' +def p_constants(p): + '''constants : STRING + | TRUE + | FALSE + | NUMBER''' + +def p_for_or_if(p): + '''for_or_if : for_loop expression %prec THEN | IF "(" expression ")" expression %prec THEN | IF "(" expression ")" expression ELSE expression - | "(" expression ")" - | call - | expression "[" expression "]" | tuple - | STRING - | NUMBER''' + ''' + +def p_expression(p): + '''expression : access_expr + | logic_expr + | list_stuff + | assert_or_echo + | assert_or_echo expression %prec ASSERT + | constants + | for_or_if + | "(" expression ")" + ''' def p_assignment_list(p): '''assignment_list : ID "=" expression @@ -179,17 +153,26 @@ def p_tuple(p): ''' tuple : "[" opt_expression_list "]" ''' +def p_commas(p): + '''commas : commas "," + | "," + ''' + def p_opt_expression_list(p): '''opt_expression_list : expression_list - | expression_list "," + | expression_list commas | empty''' def p_expression_list(p): - ''' expression_list : expression_list "," expression + ''' expression_list : expression_list commas expression | expression ''' +def p_opt_call_parameter_list(p): + '''opt_call_parameter_list : + | call_parameter_list + ''' def p_call_parameter_list(p): - '''call_parameter_list : call_parameter_list "," call_parameter + '''call_parameter_list : call_parameter_list commas call_parameter | call_parameter''' def p_call_parameter(p): @@ -198,7 +181,7 @@ def p_call_parameter(p): def p_opt_parameter_list(p): '''opt_parameter_list : parameter_list - | parameter_list "," + | parameter_list commas | empty ''' if p[1] != None: @@ -207,7 +190,7 @@ def p_opt_parameter_list(p): p[0] = [] def p_parameter_list(p): - '''parameter_list : parameter_list "," parameter + '''parameter_list : parameter_list commas parameter | parameter''' if len(p) > 2: p[0] = p[1] + [p[3]] @@ -222,7 +205,6 @@ def p_parameter(p): def p_function(p): '''function : FUNCTION ID "(" opt_parameter_list ")" "=" expression ''' - params = None if p[4] != ")": params = p[4] @@ -232,13 +214,13 @@ def p_function(p): def p_module(p): '''module : MODULE ID "(" opt_parameter_list ")" statement ''' - params = None if p[4] != ")": params = p[4] p[0] = ScadModule(p[2], params) + def p_error(p): print(f'py_scadparser: Syntax error: {p.lexer.filename}({p.lineno}) {p.type} - {p.value}') @@ -262,20 +244,12 @@ def parseFile(scadFile): for i in parser.parse(f.read(), lexer=lexer): appendObject[i.getType()](i) - return uses, includes, modules, functions, globalVars + return modules, functions, globalVars def parseFileAndPrintGlobals(scadFile): print(f'======{scadFile}======') - uses, includes, modules, functions, globalVars = parseFile(scadFile) - - print("Uses:") - for u in uses: - print(f' {u.filename}') - - print("Includes:") - for i in includes: - print(f' {i.filename}') + modules, functions, globalVars = parseFile(scadFile) print("Modules:") for m in modules: @@ -285,7 +259,7 @@ def parseFileAndPrintGlobals(scadFile): for m in functions: print(f' {m}') - print("Global Vars:") + print("Global Variables:") for m in globalVars: print(f' {m.name}') diff --git a/solid/py_scadparser/scad_tokens.py b/solid/py_scadparser/scad_tokens.py index f79bcc03..182048f5 100644 --- a/solid/py_scadparser/scad_tokens.py +++ b/solid/py_scadparser/scad_tokens.py @@ -6,7 +6,7 @@ "+", "-", "*", "/", "^", "?", ":", "[", "]", "{", "}", "(", ")", - "%", + "%", "#" ] reserved = { @@ -16,9 +16,13 @@ 'function' : 'FUNCTION', 'if' : 'IF', 'else' : 'ELSE', - 'for' : 'FOR', 'let' : 'LET', + 'assert' : 'ASSERT', + 'for' : 'FOR', 'each' : 'EACH', + 'true' : 'TRUE', + 'false' : 'FALSE', + 'echo' : 'ECHO', } tokens = [ @@ -53,8 +57,6 @@ t_FILENAME = r'<[a-zA-Z_0-9/\\\.-]*>' -t_ignore = "#$" - def t_eat_escaped_quotes(t): r"\\\"" pass @@ -74,12 +76,12 @@ def t_whitespace(t): t.lexer.lineno += t.value.count("\n") def t_ID(t): - r'[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' + r'[\$]?[0-9]*[a-zA-Z_][a-zA-Z_0-9]*' t.type = reserved.get(t.value,'ID') return t def t_NUMBER(t): - r'\d*\.?\d+' + r'[0-9]*\.?\d+([eE][-\+]\d+)?' t.value = float(t.value) return t From a779dcbf683e623edd6e03d92e50edb65171ff06 Mon Sep 17 00:00:00 2001 From: jeff <1105041+jeff-dh@users.noreply.github.com> Date: Tue, 25 May 2021 18:04:57 +0200 Subject: [PATCH 80/90] another "fix" --- solid/py_scadparser/parsetab.py | 97 +++++++++++++++--------------- solid/py_scadparser/scad_parser.py | 15 +++-- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/solid/py_scadparser/parsetab.py b/solid/py_scadparser/parsetab.py index 3ab03530..9e2b20a9 100644 --- a/solid/py_scadparser/parsetab.py +++ b/solid/py_scadparser/parsetab.py @@ -6,9 +6,9 @@ _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 assert_or_echo : ASSERT "(" opt_call_parameter_list ")"\n | ECHO "(" opt_call_parameter_list ")"\n constants : STRING\n | TRUE\n | FALSE\n | NUMBERfor_or_if : for_loop expression %prec THEN\n | IF "(" expression ")" expression %prec THEN\n | IF "(" expression ")" expression ELSE expression\n | tuple\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,62,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,210,211,212,],[-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,-63,-64,-65,42,-68,-69,-42,42,42,42,42,42,42,-55,-56,-57,-58,-62,-10,-74,42,42,4,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,-67,42,-24,-25,-26,-49,-59,42,42,4,42,4,42,-77,42,4,-23,-73,-19,42,42,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,42,-75,42,-7,-8,-76,-9,4,42,-44,4,-46,42,-52,42,42,-53,-54,42,42,-97,-60,-5,-27,42,-50,-47,-48,-96,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,62,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,210,211,212,],[-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,-63,-64,-65,57,-68,-69,-42,57,57,57,57,57,57,-55,-56,-57,-58,-62,-10,-74,57,57,6,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,-67,57,-24,-25,-26,-49,-59,57,57,6,57,6,57,-77,57,6,-23,-73,-19,57,57,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,57,-75,57,-7,-8,-76,-9,6,57,-44,6,-46,57,-52,57,57,-53,-54,57,57,-97,-60,-5,-27,57,-50,-47,-48,-96,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,62,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,210,211,212,],[-3,7,-2,-1,7,-3,7,7,7,7,7,-18,-21,-22,60,-6,60,60,7,-11,-12,-13,-14,-15,-16,-17,60,60,60,-63,-64,-65,60,-68,-69,-42,60,60,60,60,60,60,-55,-56,-57,-58,-62,-10,-74,60,60,7,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,-67,60,-24,-25,-26,-49,-59,60,60,7,60,7,60,-77,60,7,-23,-73,-19,60,60,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,60,-75,60,-7,-8,-76,-9,7,60,-44,7,-46,60,-52,60,60,-53,-54,60,60,-97,-60,-5,-27,60,-50,-47,-48,-96,60,-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,62,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,210,211,212,],[-3,8,-2,-1,8,-3,8,8,8,8,8,-18,-21,-22,61,-6,61,61,8,-11,-12,-13,-14,-15,-16,-17,61,61,61,-63,-64,-65,61,-68,-69,-42,61,61,61,61,61,61,-55,-56,-57,-58,-62,-10,-74,61,61,8,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,-67,61,-24,-25,-26,-49,-59,61,61,8,61,8,61,-77,61,8,-23,-73,-19,61,61,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,61,-75,61,-7,-8,-76,-9,8,61,-44,8,-46,61,-52,61,61,-53,-54,61,61,-97,-60,-5,-27,61,-50,-47,-48,-96,61,-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,62,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,210,211,212,],[-3,9,-2,-1,9,-3,9,9,9,9,9,-18,-21,-22,-6,9,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,9,-67,-24,-25,-26,-49,-59,9,9,9,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,9,-44,9,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,209,210,211,212,],[-3,10,-2,-1,10,-3,10,10,10,10,10,-18,-21,-22,-6,10,-11,-12,-13,-14,-15,-16,-17,91,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,91,-42,-10,91,-74,91,10,91,91,-24,-25,-26,91,91,10,10,10,-23,-73,-19,91,-70,-45,-4,-43,91,91,-28,91,91,-31,-32,-33,91,91,91,91,91,91,91,91,91,-75,-7,91,-8,91,-9,91,91,10,-44,10,-46,91,-52,91,-53,-54,-97,91,-5,91,-50,91,91,91,91,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,62,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,209,210,211,212,],[-3,11,-2,-1,11,-3,11,11,11,11,11,-18,-21,-22,-6,11,-11,-12,-13,-14,-15,-16,-17,95,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,95,-42,-10,95,-74,95,11,95,95,-24,-25,-26,95,95,11,11,11,-23,-73,-19,95,-70,-45,-4,-43,95,95,95,95,95,-31,-32,-33,95,95,95,95,95,95,95,95,95,-75,-7,95,-8,95,-9,95,95,11,-44,11,-46,95,-52,95,-53,-54,-97,95,-5,95,-50,95,95,95,95,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,62,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,210,211,212,],[-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,-63,-64,-65,55,-68,-69,-42,55,55,55,55,55,55,-55,-56,-57,-58,-62,-10,-74,55,55,12,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,-67,55,-24,-25,-26,-49,-59,55,55,12,55,12,55,-77,55,12,-23,-73,-19,55,55,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,55,-75,55,-7,-8,-76,-9,12,55,-44,12,-46,55,-52,55,55,-53,-54,55,55,-97,-60,-5,-27,55,-50,-47,-48,-96,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,62,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,210,211,212,],[-3,13,-2,-1,13,-3,13,13,13,13,13,-18,-21,-22,-6,13,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,13,-67,-24,-25,-26,-49,-59,13,13,13,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,13,-44,13,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-3,15,-2,-1,15,-3,15,15,15,15,15,-18,-21,-22,-6,15,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,15,-67,-24,-25,-26,-49,-59,15,15,15,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,15,-44,15,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-3,16,-2,-1,16,-3,16,16,16,16,16,-18,-21,-22,-6,16,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,16,-67,-24,-25,-26,-49,-59,16,16,16,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,16,-44,16,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-3,17,-2,-1,17,-3,17,17,17,17,17,-18,-21,-22,-6,17,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,128,-74,131,-93,-94,17,-67,-24,-25,-26,-49,-59,17,17,17,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,195,-92,-95,17,-44,17,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,210,211,212,],[-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,-63,-64,-65,51,-68,-69,-42,51,51,51,51,51,51,-55,-56,-57,-58,-62,-10,-74,81,81,51,73,20,143,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,-67,51,-24,-25,-26,81,68,-49,-59,73,73,20,169,51,20,73,-77,51,20,-23,-73,-19,51,81,51,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,51,-75,51,-7,-8,-76,-9,81,20,51,-44,20,-46,51,-52,51,51,-53,-54,51,81,51,-97,-60,-5,-27,51,-50,-47,-48,-96,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,62,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,210,211,212,],[-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,-63,-64,-65,21,-68,-69,-42,21,21,21,21,21,21,-55,-56,-57,-58,-62,-10,-74,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,-67,21,-24,-25,-26,-49,-59,21,21,21,21,21,21,-77,21,21,-23,-73,-19,21,21,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,21,-75,21,-7,-8,-76,-9,21,21,-44,21,-46,21,-52,21,21,-53,-54,21,21,-97,-60,-5,-27,21,-50,-47,-48,-96,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,62,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,210,211,212,],[-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,-63,-64,-65,56,-68,-69,-42,56,56,56,56,56,56,-55,-56,-57,-58,-62,-10,-74,56,56,22,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,-67,56,-24,-25,-26,-49,-59,56,56,22,56,22,56,-77,56,22,-23,-73,-19,56,56,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,56,-75,56,-7,-8,-76,-9,22,56,-44,22,-46,56,-52,56,56,-53,-54,56,56,-97,-60,-5,-27,56,-50,-47,-48,-96,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,62,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,210,211,212,],[-3,23,-2,-1,23,-3,23,23,23,23,23,-18,-21,-22,-6,23,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-74,23,-67,-24,-25,-26,-49,-59,23,23,23,-23,-73,-19,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,23,-44,23,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,62,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,211,212,],[-3,0,-2,-1,-18,-21,-22,-6,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-67,-24,-25,-26,-49,-59,-23,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,-44,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-61,-51,]),'}':([2,3,9,17,18,19,25,29,30,31,32,33,34,35,36,45,46,47,48,49,50,51,62,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,211,212,],[-2,-1,-3,-18,-21,-22,-6,75,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-67,-24,-25,-26,-49,-59,-23,-70,-45,-4,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,-44,-46,-52,-53,-54,-97,-60,-5,-27,-50,-47,-48,-96,-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,],[24,26,27,28,38,39,43,43,43,43,43,82,83,84,43,86,-63,-64,-65,43,-68,-69,-42,43,43,43,43,114,115,43,43,118,119,-55,-56,-57,-58,-62,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,-77,43,-19,43,43,86,-70,-45,-43,86,86,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,43,86,-75,43,86,-76,86,86,86,43,-44,-46,43,86,-52,86,43,43,-53,-54,43,43,86,86,43,-50,86,86,86,86,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,62,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,211,212,],[-18,-21,-22,-6,-11,-12,-13,-14,-15,-16,-17,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-10,-67,-24,-25,-26,-49,-59,-23,-70,-45,184,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,-7,-8,-9,-44,-46,-52,-53,-54,-97,208,-5,-27,-50,-47,-48,-96,-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,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,],[53,53,53,53,53,53,93,-63,-64,-65,53,-68,-69,-42,53,53,53,53,53,53,-55,-56,-57,-58,-62,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,-77,53,-19,53,53,93,-70,-45,-43,93,93,-28,-29,-30,-31,-32,-33,93,93,93,93,93,93,93,93,53,93,-75,53,93,-76,93,93,93,53,-44,-46,53,93,-52,93,53,53,-53,-54,53,53,93,93,53,-50,93,93,93,93,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,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,],[54,54,54,54,54,54,92,-63,-64,-65,54,-68,-69,-42,54,54,54,54,54,54,-55,-56,-57,-58,-62,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,-77,54,-19,54,54,92,-70,-45,-43,92,92,-28,-29,-30,-31,-32,-33,92,92,92,92,92,92,92,92,54,92,-75,54,92,-76,92,92,92,54,-44,-46,54,92,-52,92,54,54,-53,-54,54,54,92,92,54,-50,92,92,92,92,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,208,210,],[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,-77,58,-19,58,58,58,58,-76,58,58,58,58,-53,-54,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,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,],[52,52,52,52,52,52,89,-63,-64,-65,52,-68,-69,-42,52,52,52,52,52,52,-55,-56,-57,-58,-62,89,-42,89,52,89,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,-67,89,52,-24,-25,-26,-49,-59,52,52,52,52,-77,52,-19,52,52,89,-70,-45,-43,89,89,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,52,-59,-75,52,89,-76,89,89,89,52,-44,-46,52,89,-52,89,52,52,-53,-54,52,52,-60,-27,52,-50,-47,-48,89,89,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,208,210,],[62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,-77,62,-19,62,62,62,62,-76,62,62,62,62,-53,-54,62,62,62,62,-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,208,210,],[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,-77,63,-19,63,63,63,63,-76,63,63,63,63,-53,-54,63,63,63,63,-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,208,210,],[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,-77,64,-19,64,64,64,64,-76,64,64,64,64,-53,-54,64,64,64,64,-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,208,210,],[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,-77,65,-19,65,65,65,65,-76,65,65,65,65,-53,-54,65,65,65,65,-20,]),')':([27,28,38,44,45,46,47,48,49,50,51,62,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,211,212,],[-83,-83,78,87,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,120,123,-84,-86,-87,-42,127,129,130,-93,-94,-3,-3,139,141,-67,-24,-25,-26,-3,-49,-59,-83,-83,-77,179,-89,-91,181,182,-70,183,-45,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-75,190,191,192,193,-71,-85,-76,-88,-92,-95,-90,-44,-46,-52,-53,-54,-60,-27,-50,-47,-48,-72,210,-61,-51,]),'.':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[88,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,88,-42,88,88,-67,88,-24,-25,-26,-49,-59,88,-70,-45,-43,88,88,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,88,88,88,88,-44,-46,88,-52,88,-53,-54,-60,-27,-50,-47,-48,88,88,88,-61,-51,]),'?':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[90,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,90,-42,90,90,90,90,-24,-25,-26,90,90,90,-70,-45,-43,90,90,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,90,-75,90,90,90,90,-44,-46,90,-52,90,-53,-54,90,-27,-50,-47,90,90,90,90,90,-51,]),'/':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[94,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,94,-42,94,94,94,94,-24,-25,-26,94,94,94,-70,-45,-43,94,94,94,94,94,-31,-32,-33,94,94,94,94,94,94,94,94,94,-75,94,94,94,94,-44,-46,94,-52,94,-53,-54,94,94,-50,94,94,94,94,94,94,-51,]),'^':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[96,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,96,-42,96,96,96,96,-24,-25,-26,96,96,96,-70,-45,-43,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,96,-75,96,96,96,96,-44,-46,96,-52,96,-53,-54,96,96,-50,96,96,96,96,96,96,-51,]),'<':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[97,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,97,-42,97,97,97,97,-24,-25,-26,97,97,97,-70,-45,-43,97,97,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,97,97,97,-75,97,97,97,97,-44,-46,97,-52,97,-53,-54,97,97,-50,97,97,97,97,97,97,-51,]),'>':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[98,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,98,-42,98,98,98,98,-24,-25,-26,98,98,98,-70,-45,-43,98,98,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,98,98,98,-75,98,98,98,98,-44,-46,98,-52,98,-53,-54,98,98,-50,98,98,98,98,98,98,-51,]),'EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[99,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,99,-42,99,99,99,99,-24,-25,-26,99,99,99,-70,-45,-43,99,99,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,99,99,99,-75,99,99,99,99,-44,-46,99,-52,99,-53,-54,99,99,-50,99,99,99,99,99,99,-51,]),'NOT_EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[100,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,100,-42,100,100,100,100,-24,-25,-26,100,100,100,-70,-45,-43,100,100,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,100,100,100,-75,100,100,100,100,-44,-46,100,-52,100,-53,-54,100,100,-50,100,100,100,100,100,100,-51,]),'GREATER_OR_EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[101,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,101,-42,101,101,101,101,-24,-25,-26,101,101,101,-70,-45,-43,101,101,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,101,101,101,-75,101,101,101,101,-44,-46,101,-52,101,-53,-54,101,101,-50,101,101,101,101,101,101,-51,]),'LESS_OR_EQUAL':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[102,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,102,-42,102,102,102,102,-24,-25,-26,102,102,102,-70,-45,-43,102,102,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,102,102,102,-75,102,102,102,102,-44,-46,102,-52,102,-53,-54,102,102,-50,102,102,102,102,102,102,-51,]),'AND':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[103,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,103,-42,103,103,103,103,-24,-25,-26,103,103,103,-70,-45,-43,103,103,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,103,-75,103,103,103,103,-44,-46,103,-52,103,-53,-54,103,103,-50,103,103,103,103,103,103,-51,]),'OR':([44,45,46,47,48,49,50,51,62,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,209,211,212,],[104,-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,104,-42,104,104,104,104,-24,-25,-26,104,104,104,-70,-45,-43,104,104,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,104,-75,104,104,104,104,-44,-46,104,-52,104,-53,-54,104,104,-50,104,104,104,104,104,104,-51,]),',':([45,46,47,48,49,50,51,62,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,211,212,],[-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,121,125,-86,-87,-42,125,125,-93,-94,-67,-82,125,-24,-25,-26,-49,-59,173,-77,173,125,-70,125,-45,-43,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,173,121,-71,-85,-76,-88,-92,-95,173,-44,-46,-52,-81,-53,-54,-60,-27,-50,-47,-48,-72,125,-61,-51,]),':':([45,46,47,48,49,50,51,62,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,211,212,],[-63,-64,-65,-66,-68,-69,-42,-55,-56,-57,-58,-62,-67,160,-24,-25,-26,-49,-59,-70,-45,-43,186,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,-44,-46,201,-52,-53,-54,-60,-27,-50,-47,-48,-61,-51,]),']':([45,46,47,48,49,50,51,52,62,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,209,211,212,],[-63,-64,-65,-66,-68,-69,-42,-3,-55,-56,-57,-58,-62,-67,-82,162,-78,-80,-24,-25,-26,-49,-59,-77,-70,-45,-43,185,-28,-29,-30,-31,-32,-33,-34,-35,-36,-37,-38,-39,-40,-41,-59,-75,-79,-76,-44,-46,202,-52,-81,-53,-54,-60,-27,-50,-47,-48,212,-61,-51,]),} +_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,]),} _lr_action = {} for _k, _v in _lr_action_items.items(): @@ -17,7 +17,7 @@ _lr_action[_x][_k] = _y del _lr_action_items -_lr_goto_items = {'statements':([0,9,],[1,29,]),'empty':([0,9,52,82,83,114,],[2,2,110,136,136,136,]),'statement':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[3,25,30,31,32,33,34,3,142,168,171,175,197,199,]),'for_loop':([1,5,10,11,12,13,14,24,27,28,29,37,38,43,48,52,53,54,55,58,59,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,120,122,123,124,126,127,131,133,160,163,181,182,184,186,190,191,194,196,201,208,],[5,5,5,5,5,5,5,59,59,59,5,59,59,59,59,107,59,59,59,59,59,59,59,5,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,5,59,5,59,59,5,59,59,59,59,5,59,5,59,59,59,59,59,59,59,]),'call':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[14,14,14,14,14,14,14,14,14,14,14,14,14,14,]),'function':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,]),'module':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[19,19,19,19,19,19,19,19,19,19,19,19,19,19,]),'expression':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[44,72,72,76,72,85,105,106,111,112,113,116,117,138,72,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,72,72,170,72,174,176,178,187,189,198,200,203,204,205,207,209,211,]),'access_expr':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,]),'logic_expr':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,]),'list_stuff':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,]),'assert_or_echo':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,]),'constants':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,]),'for_or_if':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,]),'tuple':([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,126,131,133,160,163,182,186,190,191,194,196,201,208,],[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,66,66,66,66,66,66,66,66,66,66,66,66,66,]),'assignment_list':([26,115,],[67,165,]),'opt_call_parameter_list':([27,28,118,119,],[69,74,166,167,]),'call_parameter_list':([27,28,38,86,118,119,],[70,70,77,140,70,70,]),'call_parameter':([27,28,38,86,118,119,124,],[71,71,71,71,71,71,172,]),'parameter_list':([39,82,83,114,195,],[79,135,135,135,206,]),'parameter':([39,82,83,114,132,180,195,],[80,80,80,80,177,177,80,]),'opt_expression_list':([52,],[108,]),'expression_list':([52,],[109,]),'commas':([70,77,79,109,135,140,206,],[124,124,132,163,180,124,132,]),'opt_parameter_list':([82,83,114,],[134,137,164,]),} +_lr_goto_items = {'statements':([0,9,],[1,29,]),'empty':([0,9,52,82,83,114,],[2,2,110,136,136,136,]),'statement':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[3,25,30,31,32,33,34,3,142,168,171,175,197,199,]),'for_loop':([1,5,10,11,12,13,14,24,27,28,29,37,38,43,48,52,53,54,55,58,59,84,86,87,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,107,118,119,120,122,123,124,126,127,131,133,160,163,181,182,184,186,190,191,194,196,201,209,],[5,5,5,5,5,5,5,59,59,59,5,59,59,59,59,107,59,59,59,59,59,59,59,5,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,5,59,5,59,59,5,59,59,59,59,5,59,5,59,59,59,59,59,59,59,]),'call':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[14,14,14,14,14,14,14,14,14,14,14,14,14,14,]),'function':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[18,18,18,18,18,18,18,18,18,18,18,18,18,18,]),'module':([1,5,10,11,12,13,14,29,87,120,123,127,181,184,],[19,19,19,19,19,19,19,19,19,19,19,19,19,19,]),'expression':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[44,72,72,76,72,85,105,106,111,112,113,116,117,138,72,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,72,72,170,72,174,176,178,187,189,198,200,203,204,205,207,210,212,]),'access_expr':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,]),'logic_expr':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,]),'list_stuff':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,]),'assert_or_echo':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,]),'constants':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,]),'for_or_if':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,]),'tuple':([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,126,131,133,160,163,182,186,190,191,194,196,201,209,],[60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,]),'assignment_list':([26,115,],[67,165,]),'opt_call_parameter_list':([27,28,118,119,],[69,74,166,167,]),'call_parameter_list':([27,28,38,86,118,119,],[70,70,77,140,70,70,]),'call_parameter':([27,28,38,86,118,119,124,],[71,71,71,71,71,71,172,]),'parameter_list':([39,82,83,114,195,],[79,135,135,135,206,]),'parameter':([39,82,83,114,132,180,195,],[80,80,80,80,177,177,80,]),'opt_expression_list':([52,],[108,]),'expression_list':([52,],[109,]),'commas':([70,77,79,109,135,140,206,],[124,124,132,163,180,124,132,]),'opt_parameter_list':([82,83,114,],[134,137,164,]),'opt_else':([198,],[208,]),} _lr_goto = {} for _k, _v in _lr_goto_items.items(): @@ -79,49 +79,50 @@ ('list_stuff -> [ expression : expression ]','list_stuff',5,'p_list_stuff','scad_parser.py',110), ('list_stuff -> [ expression : expression : expression ]','list_stuff',7,'p_list_stuff','scad_parser.py',111), ('list_stuff -> [ for_loop expression ]','list_stuff',4,'p_list_stuff','scad_parser.py',112), - ('assert_or_echo -> ASSERT ( opt_call_parameter_list )','assert_or_echo',4,'p_assert_or_echo','scad_parser.py',116), - ('assert_or_echo -> ECHO ( opt_call_parameter_list )','assert_or_echo',4,'p_assert_or_echo','scad_parser.py',117), - ('constants -> STRING','constants',1,'p_constants','scad_parser.py',120), - ('constants -> TRUE','constants',1,'p_constants','scad_parser.py',121), - ('constants -> FALSE','constants',1,'p_constants','scad_parser.py',122), - ('constants -> NUMBER','constants',1,'p_constants','scad_parser.py',123), - ('for_or_if -> for_loop expression','for_or_if',2,'p_for_or_if','scad_parser.py',126), - ('for_or_if -> IF ( expression ) expression','for_or_if',5,'p_for_or_if','scad_parser.py',127), - ('for_or_if -> IF ( expression ) expression ELSE expression','for_or_if',7,'p_for_or_if','scad_parser.py',128), - ('for_or_if -> tuple','for_or_if',1,'p_for_or_if','scad_parser.py',129), - ('expression -> access_expr','expression',1,'p_expression','scad_parser.py',133), - ('expression -> logic_expr','expression',1,'p_expression','scad_parser.py',134), - ('expression -> list_stuff','expression',1,'p_expression','scad_parser.py',135), - ('expression -> assert_or_echo','expression',1,'p_expression','scad_parser.py',136), - ('expression -> assert_or_echo expression','expression',2,'p_expression','scad_parser.py',137), - ('expression -> constants','expression',1,'p_expression','scad_parser.py',138), - ('expression -> for_or_if','expression',1,'p_expression','scad_parser.py',139), - ('expression -> ( expression )','expression',3,'p_expression','scad_parser.py',140), - ('assignment_list -> ID = expression','assignment_list',3,'p_assignment_list','scad_parser.py',144), - ('assignment_list -> assignment_list , ID = expression','assignment_list',5,'p_assignment_list','scad_parser.py',145), - ('call -> ID ( call_parameter_list )','call',4,'p_call','scad_parser.py',149), - ('call -> ID ( )','call',3,'p_call','scad_parser.py',150), - ('tuple -> [ opt_expression_list ]','tuple',3,'p_tuple','scad_parser.py',153), - ('commas -> commas ,','commas',2,'p_commas','scad_parser.py',157), - ('commas -> ,','commas',1,'p_commas','scad_parser.py',158), - ('opt_expression_list -> expression_list','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',162), - ('opt_expression_list -> expression_list commas','opt_expression_list',2,'p_opt_expression_list','scad_parser.py',163), - ('opt_expression_list -> empty','opt_expression_list',1,'p_opt_expression_list','scad_parser.py',164), - ('expression_list -> expression_list commas expression','expression_list',3,'p_expression_list','scad_parser.py',166), - ('expression_list -> expression','expression_list',1,'p_expression_list','scad_parser.py',167), - ('opt_call_parameter_list -> ','opt_call_parameter_list',0,'p_opt_call_parameter_list','scad_parser.py',171), - ('opt_call_parameter_list -> call_parameter_list','opt_call_parameter_list',1,'p_opt_call_parameter_list','scad_parser.py',172), - ('call_parameter_list -> call_parameter_list commas call_parameter','call_parameter_list',3,'p_call_parameter_list','scad_parser.py',175), - ('call_parameter_list -> call_parameter','call_parameter_list',1,'p_call_parameter_list','scad_parser.py',176), - ('call_parameter -> expression','call_parameter',1,'p_call_parameter','scad_parser.py',179), - ('call_parameter -> ID = expression','call_parameter',3,'p_call_parameter','scad_parser.py',180), - ('opt_parameter_list -> parameter_list','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',183), - ('opt_parameter_list -> parameter_list commas','opt_parameter_list',2,'p_opt_parameter_list','scad_parser.py',184), - ('opt_parameter_list -> empty','opt_parameter_list',1,'p_opt_parameter_list','scad_parser.py',185), - ('parameter_list -> parameter_list commas parameter','parameter_list',3,'p_parameter_list','scad_parser.py',193), - ('parameter_list -> parameter','parameter_list',1,'p_parameter_list','scad_parser.py',194), - ('parameter -> ID','parameter',1,'p_parameter','scad_parser.py',201), - ('parameter -> ID = expression','parameter',3,'p_parameter','scad_parser.py',202), - ('function -> FUNCTION ID ( opt_parameter_list ) = expression','function',7,'p_function','scad_parser.py',206), - ('module -> MODULE ID ( opt_parameter_list ) statement','module',6,'p_module','scad_parser.py',215), + ('list_stuff -> tuple','list_stuff',1,'p_list_stuff','scad_parser.py',113), + ('assert_or_echo -> ASSERT ( opt_call_parameter_list )','assert_or_echo',4,'p_assert_or_echo','scad_parser.py',117), + ('assert_or_echo -> ECHO ( opt_call_parameter_list )','assert_or_echo',4,'p_assert_or_echo','scad_parser.py',118), + ('constants -> STRING','constants',1,'p_constants','scad_parser.py',121), + ('constants -> TRUE','constants',1,'p_constants','scad_parser.py',122), + ('constants -> FALSE','constants',1,'p_constants','scad_parser.py',123), + ('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), ] diff --git a/solid/py_scadparser/scad_parser.py b/solid/py_scadparser/scad_parser.py index f95f50d4..d56134a6 100644 --- a/solid/py_scadparser/scad_parser.py +++ b/solid/py_scadparser/scad_parser.py @@ -110,6 +110,7 @@ def p_list_stuff(p): | "[" expression ":" expression "]" | "[" expression ":" expression ":" expression "]" | "[" for_loop expression "]" + | tuple ''' def p_assert_or_echo(p): @@ -122,11 +123,14 @@ def p_constants(p): | FALSE | NUMBER''' +def p_opt_else(p): + '''opt_else : + | ELSE expression %prec THEN + ''' + #this causes some shift/reduce conflicts, but I don't know how to solve it def p_for_or_if(p): '''for_or_if : for_loop expression %prec THEN - | IF "(" expression ")" expression %prec THEN - | IF "(" expression ")" expression ELSE expression - | tuple + | IF "(" expression ")" expression opt_else ''' def p_expression(p): @@ -139,6 +143,7 @@ def p_expression(p): | for_or_if | "(" expression ")" ''' + #the assert_or_echo stuff causes some shift/reduce conflicts, but I don't know how to solve it def p_assignment_list(p): '''assignment_list : ID "=" expression @@ -226,9 +231,9 @@ def p_error(p): def parseFile(scadFile): - lexer = lex.lex() + lexer = lex.lex(debug=False) lexer.filename = scadFile - parser = yacc.yacc(debug=False, write_tables=False) + parser = yacc.yacc(debug=False) modules = [] functions = [] From 37417daac6397e7fdeab511136567536840f782a Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Thu, 30 Sep 2021 14:39:40 -0500 Subject: [PATCH 81/90] Fix for #187. `solid.utils.extrude_along_path()` was generating `polyhedron()` code that looked OK in preview, but that failed to render correctly in CGAL. This was caused by reversed face order, which would have been revealed if I'd looked at generated polyhedra with OpenSCAD's F12 'Thrown Together' view, which highlights inverted faces. Everything seems to be working well now, tests passing and the example file rendering correctly (if very slowly) in CGAL. Note that OpenSCAD can accept vertex lists for non-triangular faces, and then does its own triangulations from them. However, the direction of triangulation for a given quad isn't reliable, which can lead to some odd appearances. At the expense of some extra facets, specify our own, regular, triangulation. End caps for extruded shapes, though-- those I'm leaving to OpenSCAD to triangulate, since the existing centroid-based system added complexity without covering significantly more edge cases than OpenSCAD's native triangulation --- solid/examples/path_extrude_example.py | 9 ++-- solid/extrude_along_path.py | 57 ++++++++------------------ solid/test/test_extrude_along_path.py | 25 ++++++----- 3 files changed, 34 insertions(+), 57 deletions(-) diff --git a/solid/examples/path_extrude_example.py b/solid/examples/path_extrude_example.py index 51006578..296637c4 100755 --- a/solid/examples/path_extrude_example.py +++ b/solid/examples/path_extrude_example.py @@ -148,24 +148,23 @@ def point_trans(point: Point3, path_norm:float, loop_norm: float) -> Point3: # Rotate the points sinusoidally up to max_rotation p = p.rotate_around(up, max_rotation*sin(tau*path_norm)) - # Oscillate z values sinusoidally, growing from - # 0 magnitude to max_z_displacement - max_z = lerp(path_norm, 0, 1, 0, max_z_displacement) + # 0 magnitude to max_z_displacement, then decreasing to 0 magnitude at path_norm == 1 + max_z = sin(pi*path_norm) * max_z_displacement angle = lerp(loop_norm, 0, 1, 0, 10*tau) p.z += max_z*sin(angle) return p no_trans = make_label('No Transform') no_trans += down(height/2)( - extrude_along_path(shape, path, cap_ends=False) + extrude_along_path(shape, path, cap_ends=True) ) # We can pass transforms a single function that will be called on all points, # or pass a list with a transform function for each point along path arb_trans = make_label('Arbitrary Transform') arb_trans += down(height/2)( - extrude_along_path(shape, path, transforms=[point_trans], cap_ends=False) + extrude_along_path(shape, path, transforms=[point_trans], cap_ends=True) ) return no_trans + right(3*path_rad)(arb_trans) diff --git a/solid/extrude_along_path.py b/solid/extrude_along_path.py index 723a503b..bfdfde26 100644 --- a/solid/extrude_along_path.py +++ b/solid/extrude_along_path.py @@ -104,29 +104,18 @@ def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3 polyhedron_pts += this_loop if connect_ends: - next_loop_start_index = len(polyhedron_pts) - shape_pt_count - loop_facets = _loop_facet_indices(0, shape_pt_count, next_loop_start_index) + connect_loop_start_index = len(polyhedron_pts) - shape_pt_count + loop_facets = _loop_facet_indices(connect_loop_start_index, shape_pt_count, 0) facet_indices += loop_facets elif cap_ends: - # endcaps at start & end of extrusion - # NOTE: this block adds points & indices to the polyhedron, so it's - # very sensitive to the order this is happening in - start_cap_index = len(polyhedron_pts) - end_cap_index = start_cap_index + 1 + # OpenSCAD's polyhedron will automatically triangulate faces as needed. + # So just include all points at each end of the tube last_loop_start_index = len(polyhedron_pts) - shape_pt_count - - start_loop_pts = polyhedron_pts[:shape_pt_count] - end_loop_pts = polyhedron_pts[last_loop_start_index:] - - start_loop_indices = list(range(0, shape_pt_count)) - end_loop_indices = list(range(last_loop_start_index, last_loop_start_index + shape_pt_count)) - - start_centroid, start_facet_indices = _end_cap(start_cap_index, start_loop_pts, start_loop_indices) - end_centroid, end_facet_indices = _end_cap(end_cap_index, end_loop_pts, end_loop_indices) - polyhedron_pts += [start_centroid, end_centroid] - facet_indices += start_facet_indices - facet_indices += end_facet_indices + start_loop_indices = list(reversed(range(shape_pt_count))) + end_loop_indices = list(range(last_loop_start_index, last_loop_start_index + shape_pt_count)) + facet_indices.append(start_loop_indices) + facet_indices.append(end_loop_indices) return polyhedron(points=euc_to_arr(polyhedron_pts), faces=facet_indices) # type: ignore @@ -139,13 +128,18 @@ def _loop_facet_indices(loop_start_index:int, loop_pt_count:int, next_loop_start next_loop_indices = list(range(next_loop_start_index, loop_pt_count + next_loop_start_index )) + [next_loop_start_index] for i, (a, b) in enumerate(zip(loop_indices[:-1], loop_indices[1:])): + c, d = next_loop_indices[i: i+2] + # OpenSCAD's polyhedron will accept quads and do its own triangulation with them, + # so we could just append (a,b,d,c). + # However, this lets OpenSCAD (Or CGAL?) do its own triangulation, leading + # to some strange outcomes. Prefer to do our own triangulation. # c--d # |\ | # | \| # a--b - c, d = next_loop_indices[i: i+2] - facet_indices.append((a,c,b)) - facet_indices.append((b,c,d)) + # facet_indices.append((a,b,d,c)) + facet_indices.append((a,b,c)) + facet_indices.append((b,d,c)) return facet_indices def _rotate_loop(points:Sequence[Point3], rotation_degrees:float=None) -> List[Point3]: @@ -178,22 +172,3 @@ def _transform_loop(points:Sequence[Point3], transform_func:Point3Transform = No result.append(new_p) return result -def _end_cap(new_point_index:int, points:Sequence[Point3], vertex_indices: Sequence[int]) -> Tuple[Point3, List[FacetIndices]]: - # Assume points are a basically planar, basically convex polygon with polyhedron - # indices `vertex_indices`. - # Return a new point that is the centroid of the polygon and a list of - # vertex triangle indices that covers the whole polygon. - # (We can actually accept relatively non-planar and non-convex polygons, - # but not anything pathological. Stars are fine, internal pockets would - # cause incorrect faceting) - - # NOTE: In order to deal with moderately-concave polygons, we add a point - # to the center of the end cap. This will have a new index that we require - # as an argument. - - new_point = centroid(points) - new_facets = [] - second_indices = vertex_indices[1:] + [vertex_indices[0]] - new_facets = [(new_point_index, a, b) for a, b in zip(vertex_indices, second_indices)] - - return (new_point, new_facets) diff --git a/solid/test/test_extrude_along_path.py b/solid/test/test_extrude_along_path.py index 1528f704..e4f86a21 100755 --- a/solid/test/test_extrude_along_path.py +++ b/solid/test/test_extrude_along_path.py @@ -28,14 +28,14 @@ def test_extrude_along_path(self): path = [[0, 0, 0], [0, 20, 0]] # basic test actual = extrude_along_path(tri, path) - expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[10.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_vertical(self): # make sure we still look good extruding along z axis; gimbal lock can mess us up vert_path = [[0, 0, 0], [0, 0, 20]] actual = extrude_along_path(tri, vert_path) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[-10.0000000000,0.0000000000,0.0000000000],[0.0000000000,10.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[-10.0000000000,0.0000000000,20.0000000000],[0.0000000000,10.0000000000,20.0000000000],[-3.3333333333,3.3333333333,0.0000000000],[-3.3333333333,3.3333333333,20.0000000000]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[-10.0000000000,0.0000000000,0.0000000000],[0.0000000000,10.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[-10.0000000000,0.0000000000,20.0000000000],[0.0000000000,10.0000000000,20.0000000000]]); ' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_1d_scale(self): @@ -43,7 +43,9 @@ def test_extrude_along_path_1d_scale(self): path = [[0, 0, 0], [0, 20, 0]] scales_1d = [1.5, 0.5] actual = extrude_along_path(tri, path, scales=scales_1d) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[15.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,15.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,5.0000000000],[5.0000000000,0.0000000000,5.0000000000],[1.6666666667,20.0000000000,1.6666666667]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[15.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,15.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,5.0000000000]]);' + print('test_extrude_along_path_1d_scale') + self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_2d_scale(self): @@ -51,7 +53,7 @@ def test_extrude_along_path_2d_scale(self): path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] scales_2d = [Point2(1,1), Point2(0.5, 1.5), Point2(1.5, 0.5), ] actual = extrude_along_path(tri, path, scales=scales_2d) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[2,1,0],[6,7,8]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_2d_scale_list_input(self): @@ -59,34 +61,35 @@ def test_extrude_along_path_2d_scale_list_input(self): path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] scales_2d = [(1,1), (0.5, 1.5), (1.5, 0.5), ] actual = extrude_along_path(tri, path, scales=scales_2d) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[9,0,1],[9,1,2],[9,2,0],[10,6,7],[10,7,8],[10,8,6]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000],[3.3333333333,0.0000000000,3.3333333333],[5.0000000000,40.0000000000,1.6666666667]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[2,1,0],[6,7,8]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_end_caps(self): path = [[0, 0, 0], [0, 20, 0]] actual = scad_render(extrude_along_path(tri, path, connect_ends=False)) - expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [10.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 10.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [10.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [3.3333333333, 0.0000000000, 3.3333333333], [3.3333333333, 20.0000000000, 3.3333333333]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[10.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]); ' self.assertEqualNoWhitespace(expected, actual) def test_extrude_along_path_connect_ends(self): path = [[0, 0, 0], [20, 0, 0], [20,20,0], [0,20, 0]] actual = extrude_along_path(tri, path, connect_ends=True) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[3,6,4],[4,6,7],[4,7,5],[5,7,8],[5,8,3],[3,8,6],[6,9,7],[7,9,10],[7,10,8],[8,10,11],[8,11,6],[6,11,9],[0,9,1],[1,9,10],[1,10,2],[2,10,11],[2,11,0],[0,11,9]],points=[[0.0000000000,0.0000000000,0.0000000000],[-7.0710678119,-7.0710678119,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[27.0710678119,-7.0710678119,0.0000000000],[20.0000000000,0.0000000000,10.0000000000],[20.0000000000,20.0000000000,0.0000000000],[27.0710678119,27.0710678119,0.0000000000],[20.0000000000,20.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[-7.0710678119,27.0710678119,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[8,6,11],[6,9,11],[9,10,0],[10,1,0],[10,11,1],[11,2,1],[11,9,2],[9,0,2]],points=[[0.0000000000,0.0000000000,0.0000000000],[-7.0710678119,-7.0710678119,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[27.0710678119,-7.0710678119,0.0000000000],[20.0000000000,0.0000000000,10.0000000000],[20.0000000000,20.0000000000,0.0000000000],[27.0710678119,27.0710678119,0.0000000000],[20.0000000000,20.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[-7.0710678119,27.0710678119,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]); ' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_rotations(self): + # confirm we can rotate for each point in path path = [[0,0,0], [20, 0,0 ]] rotations = [-45, 45] actual = extrude_along_path(tri, path, rotations=rotations) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-7.0710678119,-7.0710678119],[0.0000000000,-7.0710678119,7.0710678119],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119],[0.0000000000,-4.7140452079,0.0000000000],[20.0000000000,-0.0000000000,4.7140452079]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-7.0710678119,-7.0710678119],[0.0000000000,-7.0710678119,7.0710678119],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119]]); ' self.assertEqualOpenScadObject(expected, actual) # confirm we can rotate with a single supplied value path = [[0,0,0], [20, 0,0 ]] rotations = [45] actual = extrude_along_path(tri, path, rotations=rotations) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-10.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119],[0.0000000000,-3.3333333333,3.3333333333],[20.0000000000,-0.0000000000,4.7140452079]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-10.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119]]); ' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_transforms(self): @@ -95,13 +98,13 @@ def test_extrude_along_path_transforms(self): # Make sure we can take a transform function for each point in path transforms = [lambda p, path, loop: 2*p, lambda p, path, loop: 0.5*p] actual = extrude_along_path(tri, path, transforms=transforms) - expected = 'polyhedron(faces=[[0,3,1],[1,3,4],[1,4,2],[2,4,5],[2,5,0],[0,5,3],[6,0,1],[6,1,2],[6,2,0],[7,3,4],[7,4,5],[7,5,3]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-5.0000000000,0.0000000000],[20.0000000000,0.0000000000,5.0000000000],[0.0000000000,-6.6666666667,6.6666666667],[20.0000000000,-1.6666666667,1.6666666667]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-5.0000000000,0.0000000000],[20.0000000000,0.0000000000,5.0000000000]]); ' self.assertEqualOpenScadObject(expected, actual) # Make sure we can take a single transform function for all points transforms = [lambda p, path, loop: 2*p] actual = extrude_along_path(tri, path, transforms=transforms) - expected = 'polyhedron(faces = [[0, 3, 1], [1, 3, 4], [1, 4, 2], [2, 4, 5], [2, 5, 0], [0, 5, 3], [6, 0, 1], [6, 1, 2], [6, 2, 0], [7, 3, 4], [7, 4, 5], [7, 5, 3]], points = [[0.0000000000, 0.0000000000, 0.0000000000], [0.0000000000, -20.0000000000, 0.0000000000], [0.0000000000, 0.0000000000, 20.0000000000], [20.0000000000, 0.0000000000, 0.0000000000], [20.0000000000, -20.0000000000, 0.0000000000], [20.0000000000, 0.0000000000, 20.0000000000], [0.0000000000, -6.6666666667, 6.6666666667], [20.0000000000, -6.6666666667, 6.6666666667]]);' + expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-20.0000000000,0.0000000000],[20.0000000000,0.0000000000,20.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_numpy(self): From 5690267239ecbfeb189a0f6be45e9182a831c2d9 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 29 Nov 2021 11:49:13 -0600 Subject: [PATCH 82/90] Fix for #189. `polyhedron()` convexity defaults to 10. This prevents some situations in the CSG visualizer where back faces were shown incorrectly. Changes to make tests work as well. --- solid/objects.py | 2 +- solid/test/test_extrude_along_path.py | 23 +++++++++++------------ solid/test/test_solidpython.py | 1 + 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index 79aff238..6b38e0cc 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -219,7 +219,7 @@ class polyhedron(OpenSCADObject): :type convexity: int """ - def __init__(self, points: P3s, faces: Indexes, convexity: int = None, triangles: Indexes = None) -> None: + def __init__(self, points: P3s, faces: Indexes, convexity: int = 10, triangles: Indexes = None) -> None: super().__init__('polyhedron', {'points': points, 'faces': faces, 'convexity': convexity, diff --git a/solid/test/test_extrude_along_path.py b/solid/test/test_extrude_along_path.py index e4f86a21..2b935b5b 100755 --- a/solid/test/test_extrude_along_path.py +++ b/solid/test/test_extrude_along_path.py @@ -28,14 +28,14 @@ def test_extrude_along_path(self): path = [[0, 0, 0], [0, 20, 0]] # basic test actual = extrude_along_path(tri, path) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[10.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]);' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[10.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_vertical(self): # make sure we still look good extruding along z axis; gimbal lock can mess us up vert_path = [[0, 0, 0], [0, 0, 20]] actual = extrude_along_path(tri, vert_path) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[-10.0000000000,0.0000000000,0.0000000000],[0.0000000000,10.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[-10.0000000000,0.0000000000,20.0000000000],[0.0000000000,10.0000000000,20.0000000000]]); ' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[-10.0000000000,0.0000000000,0.0000000000],[0.0000000000,10.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[-10.0000000000,0.0000000000,20.0000000000],[0.0000000000,10.0000000000,20.0000000000]]); ' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_1d_scale(self): @@ -43,8 +43,7 @@ def test_extrude_along_path_1d_scale(self): path = [[0, 0, 0], [0, 20, 0]] scales_1d = [1.5, 0.5] actual = extrude_along_path(tri, path, scales=scales_1d) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[15.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,15.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,5.0000000000]]);' - print('test_extrude_along_path_1d_scale') + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[15.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,15.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,5.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) @@ -53,7 +52,7 @@ def test_extrude_along_path_2d_scale(self): path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] scales_2d = [Point2(1,1), Point2(0.5, 1.5), Point2(1.5, 0.5), ] actual = extrude_along_path(tri, path, scales=scales_2d) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[2,1,0],[6,7,8]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000]]);' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[2,1,0],[6,7,8]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_2d_scale_list_input(self): @@ -61,19 +60,19 @@ def test_extrude_along_path_2d_scale_list_input(self): path = [[0, 0, 0], [0, 20, 0], [0, 40, 0]] scales_2d = [(1,1), (0.5, 1.5), (1.5, 0.5), ] actual = extrude_along_path(tri, path, scales=scales_2d) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[2,1,0],[6,7,8]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000]]);' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[2,1,0],[6,7,8]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[5.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,15.0000000000],[0.0000000000,40.0000000000,0.0000000000],[15.0000000000,40.0000000000,0.0000000000],[0.0000000000,40.0000000000,5.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_end_caps(self): path = [[0, 0, 0], [0, 20, 0]] actual = scad_render(extrude_along_path(tri, path, connect_ends=False)) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[10.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]); ' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[10.0000000000,0.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[10.0000000000,20.0000000000,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]); ' self.assertEqualNoWhitespace(expected, actual) def test_extrude_along_path_connect_ends(self): path = [[0, 0, 0], [20, 0, 0], [20,20,0], [0,20, 0]] actual = extrude_along_path(tri, path, connect_ends=True) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[8,6,11],[6,9,11],[9,10,0],[10,1,0],[10,11,1],[11,2,1],[11,9,2],[9,0,2]],points=[[0.0000000000,0.0000000000,0.0000000000],[-7.0710678119,-7.0710678119,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[27.0710678119,-7.0710678119,0.0000000000],[20.0000000000,0.0000000000,10.0000000000],[20.0000000000,20.0000000000,0.0000000000],[27.0710678119,27.0710678119,0.0000000000],[20.0000000000,20.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[-7.0710678119,27.0710678119,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]); ' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[3,4,6],[4,7,6],[4,5,7],[5,8,7],[5,3,8],[3,6,8],[6,7,9],[7,10,9],[7,8,10],[8,11,10],[8,6,11],[6,9,11],[9,10,0],[10,1,0],[10,11,1],[11,2,1],[11,9,2],[9,0,2]],points=[[0.0000000000,0.0000000000,0.0000000000],[-7.0710678119,-7.0710678119,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[27.0710678119,-7.0710678119,0.0000000000],[20.0000000000,0.0000000000,10.0000000000],[20.0000000000,20.0000000000,0.0000000000],[27.0710678119,27.0710678119,0.0000000000],[20.0000000000,20.0000000000,10.0000000000],[0.0000000000,20.0000000000,0.0000000000],[-7.0710678119,27.0710678119,0.0000000000],[0.0000000000,20.0000000000,10.0000000000]]); ' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_rotations(self): @@ -82,14 +81,14 @@ def test_extrude_along_path_rotations(self): path = [[0,0,0], [20, 0,0 ]] rotations = [-45, 45] actual = extrude_along_path(tri, path, rotations=rotations) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-7.0710678119,-7.0710678119],[0.0000000000,-7.0710678119,7.0710678119],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119]]); ' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-7.0710678119,-7.0710678119],[0.0000000000,-7.0710678119,7.0710678119],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119]]); ' self.assertEqualOpenScadObject(expected, actual) # confirm we can rotate with a single supplied value path = [[0,0,0], [20, 0,0 ]] rotations = [45] actual = extrude_along_path(tri, path, rotations=rotations) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-10.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119]]); ' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-10.0000000000,0.0000000000],[0.0000000000,0.0000000000,10.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-7.0710678119,7.0710678119],[20.0000000000,7.0710678119,7.0710678119]]); ' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_transforms(self): @@ -98,13 +97,13 @@ def test_extrude_along_path_transforms(self): # Make sure we can take a transform function for each point in path transforms = [lambda p, path, loop: 2*p, lambda p, path, loop: 0.5*p] actual = extrude_along_path(tri, path, transforms=transforms) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-5.0000000000,0.0000000000],[20.0000000000,0.0000000000,5.0000000000]]); ' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-5.0000000000,0.0000000000],[20.0000000000,0.0000000000,5.0000000000]]); ' self.assertEqualOpenScadObject(expected, actual) # Make sure we can take a single transform function for all points transforms = [lambda p, path, loop: 2*p] actual = extrude_along_path(tri, path, transforms=transforms) - expected = 'polyhedron(faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-20.0000000000,0.0000000000],[20.0000000000,0.0000000000,20.0000000000]]);' + expected = 'polyhedron(convexity=10,faces=[[0,1,3],[1,4,3],[1,2,4],[2,5,4],[2,0,5],[0,3,5],[2,1,0],[3,4,5]],points=[[0.0000000000,0.0000000000,0.0000000000],[0.0000000000,-20.0000000000,0.0000000000],[0.0000000000,0.0000000000,20.0000000000],[20.0000000000,0.0000000000,0.0000000000],[20.0000000000,-20.0000000000,0.0000000000],[20.0000000000,0.0000000000,20.0000000000]]);' self.assertEqualOpenScadObject(expected, actual) def test_extrude_along_path_numpy(self): diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 08f6af32..e2b7dec0 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -28,6 +28,7 @@ {'name': 'cylinder', 'class': 'cylinder' , 'kwargs': {'r1': None, 'r2': None, 'h': 1, 'segments': 12, 'r': 1, 'center': False}, 'expected': '\n\ncylinder($fn = 12, center = false, h = 1, r = 1);', 'args': {}, }, {'name': 'cylinder_d1d2', 'class': 'cylinder' , 'kwargs': {'d1': 4, 'd2': 2, 'h': 1, 'segments': 12, 'center': False}, 'expected': '\n\ncylinder($fn = 12, center = false, d1 = 4, d2 = 2, h = 1);', 'args': {}, }, {'name': 'polyhedron', 'class': 'polyhedron' , 'kwargs': {'convexity': None}, 'expected': '\n\npolyhedron(faces = [[0, 1, 2]], points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]], 'faces': [[0, 1, 2]]}, }, + {'name': 'polyhedron_default_convexity', 'class': 'polyhedron' , 'kwargs': {}, 'expected': '\n\npolyhedron(convexity = 10, faces = [[0, 1, 2]], points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]);', 'args': {'points': [[0, 0, 0], [1, 0, 0], [0, 1, 0]], 'faces': [[0, 1, 2]]}, }, {'name': 'union', 'class': 'union' , 'kwargs': {}, 'expected': '\n\nunion();', 'args': {}, }, {'name': 'intersection', 'class': 'intersection' , 'kwargs': {}, 'expected': '\n\nintersection();', 'args': {}, }, {'name': 'difference', 'class': 'difference' , 'kwargs': {}, 'expected': '\n\ndifference();', 'args': {}, }, From 4715c827ad90db26ee37df57bc425e6f2de3cf8d Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 3 Jan 2022 09:45:46 -0600 Subject: [PATCH 83/90] Version bump to v1.1.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3368907b..e29373aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.1.1" +version = "1.1.2" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From 2d5476318150b8da0d6e3ac6a66d8b7c95de5516 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 15 Feb 2022 13:25:30 -0600 Subject: [PATCH 84/90] Resolves #176 & #194. Use the first-found, not last-found include dir, and look in default app-install OpenSCAD `libraries` directories as well as per-user libraries dirs --- solid/objects.py | 23 ++++++++++++++++++----- solid/test/test_solidpython.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index 6b38e0cc..22ecd2ed 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -830,14 +830,24 @@ def _openscad_library_paths() -> List[Path]: paths.append(Path(s)) default_paths = { - 'Linux': Path.home() / '.local/share/OpenSCAD/libraries', - 'Darwin': Path.home() / 'Documents/OpenSCAD/libraries', - 'Windows': Path('My Documents\OpenSCAD\libraries') + 'Linux': [ + Path.home() / '.local/share/OpenSCAD/libraries', + Path('/usr/share/openscad/libraries') + ], + 'Darwin': [ + Path.home() / 'Documents/OpenSCAD/libraries', + Path('/Applications/OpenSCAD.app/Contents/Resources/libraries') + ], + 'Windows': [ + Path('My Documents\OpenSCAD\libraries'), + Path('c:\Program Files\OpenSCAD\libraries') + ], } - paths.append(default_paths[platform.system()]) + paths += default_paths.get(platform.system(), []) return paths + def _find_library(library_name: PathStr) -> Path: result = Path(library_name) @@ -848,14 +858,17 @@ def _find_library(library_name: PathStr) -> Path: # print(f'Checking {f} -> {f.exists()}') if f.exists(): result = f + break return result - + # use() & include() mimic OpenSCAD's use/include mechanics. # -- use() makes methods in scad_file_path.scad available to be called. # --include() makes those methods available AND executes all code in # scad_file_path.scad, which may have side effects. # Unless you have a specific need, call use(). + + def use(scad_file_path: PathStr, use_not_include: bool = True, dest_namespace_dict: Dict = None): """ Opens scad_file_path, parses it for all usable calls, diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index e2b7dec0..029c8bb5 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -233,14 +233,38 @@ def test_import_scad(self): self.assertTrue(hasattr(examples, 'scad_to_include')) self.assertTrue(hasattr(examples.scad_to_include, 'steps')) - # TODO: we should test that: + # Test that: # A) scad files in the designated OpenSCAD library directories # (path-dependent, see: solid.objects._openscad_library_paths()) - # are imported correctly. Not sure how to do this without writing - # temp files to those directories. Seems like overkill for the moment + # are imported correctly. + # B) scad files in the designated app-install library directories + from solid import objects + lib_dirs = objects._openscad_library_paths() + for i, ld in enumerate(lib_dirs): + if ld.as_posix() == '.': + continue + if not ld.exists(): + continue + temp_dirname = f'test_{i}' + d = ld / temp_dirname + d.mkdir(exist_ok=True) + p = d / 'scad_to_include.scad' + p.write_text(include_file.read_text()) + temp_file_str = f'{temp_dirname}/scad_to_include.scad' + + mod = import_scad(temp_file_str) + a = mod.steps(3) + actual = scad_render(a) + expected = f"use <{p.absolute()}>\n\n\nsteps(howmany = 3);" + self.assertEqual(actual, expected, + f'Unexpected file contents at {p} for dir: {ld}') + + # remove generated file and directories + p.unlink() + d.rmdir() def test_multiple_import_scad(self): - # For Issue #172. Originally, multiple `import_scad()` calls would + # For Issue #172. Originally, multiple `import_scad()` calls would # re-import the entire module, rather than cache a module after one use include_file = self.expand_scad_path("examples/scad_to_include.scad") mod1 = import_scad(include_file) From 0a4f539c31a25df52a42bab2ceeffafd45596f73 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 15 Feb 2022 13:26:28 -0600 Subject: [PATCH 85/90] version bump => 1.1.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e29373aa..6554128a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "solidpython" -version = "1.1.2" +version = "1.1.3" description = "Python interface to the OpenSCAD declarative geometry language" authors = ["Evan Jones "] homepage = "https://github.com/SolidCode/SolidPython" From d6568ee7e1f85025725597d1cdcb62b3d0a1ded1 Mon Sep 17 00:00:00 2001 From: David Runge Date: Mon, 9 Jan 2023 20:09:25 +0100 Subject: [PATCH 86/90] Switch to correct PEP517 build-system pyproject.toml: Since poetry is used, switch to the build-system setup as documented upstream (https://python-poetry.org/docs/pyproject#poetry-and-pep-517). --- pyproject.toml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6554128a..1ab76178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,17 +44,5 @@ tox = "^tox 3.11" [build-system] -requires = [ - "poetry>=0.12", - # See https://github.com/pypa/setuptools/issues/2353#issuecomment-683781498 - # for the rest of these requirements, - # -ETJ 31 December 2020 - "setuptools>=30.3.0,<50", - "wheel", - "pytest-runner", - "setuptools_scm>=3.3.1", - -] - -build-backend = "poetry.masonry.api" - +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From 025ca1c1bd9848bf12bff72b80b8d05d8e24968d Mon Sep 17 00:00:00 2001 From: David Runge Date: Mon, 9 Jan 2023 20:25:43 +0100 Subject: [PATCH 87/90] Add setuptools to dependencies pyproject.toml: Pkg_resources is used in solid/solidpython.py, hence this project depends on setuptools until the use of pkg_resources is replaced with something else. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1ab76178..8b31bb73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ euclid3 = "^0.1.0" pypng = "^0.0.19" PrettyTable = "=0.7.2" ply = "^3.11" +setuptools = ">=65.6.3" [tool.poetry.dev-dependencies] tox = "^tox 3.11" From 594d81ff745cf9554641390be25b254272b5f81e Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Mon, 9 Jan 2023 16:13:39 -0600 Subject: [PATCH 88/90] Resolves #201. extrude_along_path() extrusions where first & last rail points were the same had some ugly joins whether or not `connect_ends` was set. If first and last points are identical (within sqrt(EPSILON) of each other), assume we want to connect ends, and remove the duplicate rail point. --- solid/extrude_along_path.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/solid/extrude_along_path.py b/solid/extrude_along_path.py index bfdfde26..bdd94760 100644 --- a/solid/extrude_along_path.py +++ b/solid/extrude_along_path.py @@ -1,7 +1,7 @@ #! /usr/bin/env python from math import radians from solid import OpenSCADObject, Points, Indexes, ScadSize, polyhedron -from solid.utils import euclidify, euc_to_arr, transform_to_point, centroid +from solid.utils import euclidify, euc_to_arr, transform_to_point, centroid, EPSILON from euclid3 import Point2, Point3, Vector2, Vector3 from typing import Dict, Optional, Sequence, Tuple, Union, List, Callable @@ -64,6 +64,13 @@ def transform_func(p:Point3, path_norm:float, loop_norm:float): Point3 shape_pt_count = len(shape_pts) tangent_path_points: List[Point3] = [] + + # If first & last points are the same, let's close the shape + first_last_equal = ((path_pts[0] - path_pts[-1]).magnitude_squared() < EPSILON) + if first_last_equal: + connect_ends = True + path_pts = path_pts[:][:-1] + if connect_ends: tangent_path_points = [path_pts[-1]] + path_pts + [path_pts[0]] else: From bba953acd02b7049d1a7ad12d9417ce37451a78e Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Fri, 10 Mar 2023 15:29:44 -0600 Subject: [PATCH 89/90] Update README.rst to point to SolidPython V2 --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index e9a4ba07..2fb6a1d9 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,7 @@ +Hey! All the energy and improvements in this project are going into SolidPython V2. Check it out at [Github](https://github.com/jeff-dh/SolidPython) or on its [PyPI page](https://pypi.org/project/solidpython2/) before you commit to an older version. + + + SolidPython ----------- From d962740d600c5dfd69458c4559fc416b9beab575 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Fri, 10 Mar 2023 15:34:11 -0600 Subject: [PATCH 90/90] Correction: rst syntax in links, not Markdown --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2fb6a1d9..946ed134 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Hey! All the energy and improvements in this project are going into SolidPython V2. Check it out at [Github](https://github.com/jeff-dh/SolidPython) or on its [PyPI page](https://pypi.org/project/solidpython2/) before you commit to an older version. +**Hey!** All the energy and improvements in this project are going into **SolidPython V2**. Check it out at `Github `_ or on its `PyPI page `_ before you commit to an older version.