From 2948a5627009fc2bcde9e5435804d2a6addc2ccd Mon Sep 17 00:00:00 2001 From: BarnacleDuck Date: Sat, 18 Mar 2017 23:34:53 +1100 Subject: [PATCH 1/2] Hijack existing part class to allow the storage of dimension (and/or other data) within the part. This allows a solid object to be queried by the balance of the program. This also requires the addition of two methods to the OpenSCADObject class - the first to find a part identified by id in an existing tree - the second to get a named dimension from a part identified by id. --- solid/objects.py | 71 +++++++++++++++++++++++++++++++++- solid/solidpython.py | 19 ++++++++- solid/test/test_solidpython.py | 37 ++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index 70b067be..386a6fa3 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -3,6 +3,9 @@ """ from .solidpython import OpenSCADObject from .solidpython import IncludedOpenSCADObject +import inspect +import uuid +from collections import OrderedDict class polygon(OpenSCADObject): ''' @@ -193,10 +196,76 @@ def __init__(self): class part(OpenSCADObject): - def __init__(self): + """ + Extended part object. + Intended to be called from within a function dedicated to assembling the part. + When the object assembly is completed, part is called on it. This creates a solid python "part" + - in effect holes created in the assembly can be filled by CSG operations with other objects. + The part stores extra information about parameters used to create the part. This allows + the object to be queried for the creation of, eg, mating parts. For example, specifying an internal + screw thread might permit the on-the-fly calculation of a matching external thread. + + Brendan Barnacle Duck, March 17 + """ + + def __init__(self, part_id=None, dims_dict=None, function_name=None): + """ + param part_id: unique identifier for this part + type part_id: string + param dims_dict: dictionary of measurements that define the part + type dims_dict: dictionary of float + param function_name: name of function used to assemble this part + type function_name: string + + Note on dimension guessing code: + Code to determine the arg values, function name is apparently implementation (CPython) dependent, so + pass express values if that fails. + The code also depnds on the function doing the assembly not accepting * or ** args + The dimension values are taken from the function's arguments and their values as at the time of the call + to part. So, better to not change their values before the call to part. + + """ + OpenSCADObject.__init__(self, 'part', {}) self.set_part_root(True) + if function_name is None: + function_name = (inspect.stack()[1][3]) + + if part_id is None: + part_id = str(uuid.uuid4()) # random uuid + + # dimension guessing + if dims_dict is None: + # then try to grab them from what was provided to the calling function + # - ie the function that assembled this part. + args, varargs, keywords, _locals = inspect.getargvalues(inspect.stack()[1][0]) + # filter out non-parameter locals + dims_dict = OrderedDict() # preserve the order of mandatory/optional args for __repr__ + for k in args: + dims_dict[k]=_locals[k] + + self.part_id = part_id + self.part_dims = dims_dict + self.function_name = function_name + + @property + def part_signature(self): + return (self.function_name, self.part_dims) + + def __repr__(self): + args = [] + for k,v in self.part_dims.items(): + try: + # it's a number + spam = float(v) + args.append((k,v)) + except ValueError: + # its a string + args.append((k,"""'%s'"""%v)) + + return "%s(%s)"%(self.function_name, ", ".join(["{d[0]}={d[1]}".format(d=a) for a in args])) + class translate(OpenSCADObject): ''' diff --git a/solid/solidpython.py b/solid/solidpython.py index 1a0e89ef..c6e13d78 100755 --- a/solid/solidpython.py +++ b/solid/solidpython.py @@ -8,7 +8,6 @@ # License: LGPL 2.1 or later # - import os, sys, re import inspect import subprocess @@ -571,6 +570,24 @@ def _repr_png_(self): return png_data + def get_part(self, part_id): + """ find child with id = part_id""" + if self.is_part_root: + if self.part_id == part_id: + return self + else: + for c in self.children: + p = c.get_part(part_id) + if p is not None: + return p + return None + + def get_part_dim(self, part_id, dim_name): + """Find the value of var_name for part with id = part_id + """ + p = self.get_part(part_id) + return p.part_dims[dim_name] + class IncludedOpenSCADObject(OpenSCADObject): # Identical to OpenSCADObject, but each subclass of IncludedOpenSCADObject diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index 72ce465f..e0fba262 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -7,6 +7,9 @@ import unittest import tempfile from solid.test.ExpandedTestCase import DiffOutput + +import os,sys,inspect + from solid import * scad_test_case_templates = [ @@ -267,6 +270,40 @@ def test_scad_render_to_file(self): # be done from a separate file, or include everything in this one + + def test_extended_part(self): + # create part, __repr__ works as expected + part1 = make_a_part("part1") + actual = part1.__repr__() + expected = """make_a_part(partid='part1', width=20, length=40, depth=10)""" + self.assertEqual(expected, actual) + + test_sphere = sphere(1) + obj = union()(part1, test_sphere) + obj = translate([10,10,10])(obj) + obj = rotate([90,0,0])(obj) + + # get part correctly traverses object tree to find part1 + part2 = obj.get_part("part1") + self.assertEqual(part1, part2) + + width = obj.get_part_dim("part1", "width") + self.assertEqual(width, 20) + + +def make_a_part(partid, width=20, length=40, depth=10): + obj = cube([width, length, depth]) + + # additional locals that need to be not recorded in the part + half_width = width/2 + half_length = length/2 + half_depth = depth/2 + obj = part(part_id="part1")(obj) + + return obj + + + def single_test(test_dict): name, args, kwargs, expected = test_dict['name'], test_dict['args'], test_dict['kwargs'], test_dict['expected'] From 3bb1d22680ff2c6b082a35b96d5f3ee147feba43 Mon Sep 17 00:00:00 2001 From: BarnacleDuck Date: Sun, 19 Mar 2017 18:41:52 +1100 Subject: [PATCH 2/2] Additional test, comment correction Added a test for signture property, corrected a comment in part definition --- solid/objects.py | 11 ++++++++--- solid/test/test_solidpython.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/solid/objects.py b/solid/objects.py index 386a6fa3..560feb8c 100644 --- a/solid/objects.py +++ b/solid/objects.py @@ -203,7 +203,12 @@ class part(OpenSCADObject): - in effect holes created in the assembly can be filled by CSG operations with other objects. The part stores extra information about parameters used to create the part. This allows the object to be queried for the creation of, eg, mating parts. For example, specifying an internal - screw thread might permit the on-the-fly calculation of a matching external thread. + screw thread might permit the on-the-fly calculation of a matching external thread. + + [Once a part allows data storage and introspection, it will facilitate other uses - such as nominating + part connectors, re-orienting parts to mate them together at specfied connectors, splitting out + separate parts from an object and arranging them on a print bed for printing, in place rotation around + a nominated centre or rotation etc] Brendan Barnacle Duck, March 17 """ @@ -235,7 +240,7 @@ def __init__(self, part_id=None, dims_dict=None, function_name=None): if part_id is None: part_id = str(uuid.uuid4()) # random uuid - # dimension guessing + # if dimesions are not provided explicitly, try to deduce them from the calling function if dims_dict is None: # then try to grab them from what was provided to the calling function # - ie the function that assembled this part. @@ -260,7 +265,7 @@ def __repr__(self): # it's a number spam = float(v) args.append((k,v)) - except ValueError: + except (ValueError, TypeError): # its a string args.append((k,"""'%s'"""%v)) diff --git a/solid/test/test_solidpython.py b/solid/test/test_solidpython.py index e0fba262..dc7aa246 100755 --- a/solid/test/test_solidpython.py +++ b/solid/test/test_solidpython.py @@ -272,23 +272,49 @@ def test_scad_render_to_file(self): def test_extended_part(self): - # create part, __repr__ works as expected + # create part, does not interfere with rendering part1 = make_a_part("part1") + actual = scad_render(part1) + expected = """\n\ncube(size = [20, 40, 10]);""" + self.assertEqual(actual, expected) + + # __repr__ works actual = part1.__repr__() expected = """make_a_part(partid='part1', width=20, length=40, depth=10)""" self.assertEqual(expected, actual) + + # get part signature + actual = part1.part_signature + expected = ('make_a_part', OrderedDict([('partid', 'part1'), ('width', 20), ('length', 40), ('depth', 10)])) + self.assertEqual(actual, expected) + # get part correctly traverses object tree to find part1 test_sphere = sphere(1) obj = union()(part1, test_sphere) obj = translate([10,10,10])(obj) obj = rotate([90,0,0])(obj) - # get part correctly traverses object tree to find part1 part2 = obj.get_part("part1") self.assertEqual(part1, part2) + # correctly get part dimension width = obj.get_part_dim("part1", "width") self.assertEqual(width, 20) + + #creation of a part without arguments + # will create default dimensions etc, which can't be relied on + test_sphere = part()(test_sphere) # no failure on creation + actual = scad_render(test_sphere) + expected = "\n\nsphere(r = 1);" + self.assertEqual(actual, expected) + + actual = test_sphere.__repr__() + expected = """test_extended_part(self='test_extended_part (__main__.TestSolidPython)')""" + self.assertEqual(actual, expected) + + # this can't be tested from __main__ in this test rig, but manual testing doesn't fail + # when parts are declared in __main__ rather than an explicit funciton as the class anticipates + def make_a_part(partid, width=20, length=40, depth=10):