diff --git a/solid/objects.py b/solid/objects.py index 70b067be..560feb8c 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,81 @@ 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. + + [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 + """ + + 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 + + # 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. + 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, TypeError): + # 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..dc7aa246 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,66 @@ 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, 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) + + 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): + 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']