From 1b2510250aa06f5821daf45ac6a36976c0ee2b45 Mon Sep 17 00:00:00 2001 From: Roman Kuzmenko Date: Sat, 6 Jan 2024 13:46:14 -0800 Subject: [PATCH 1/2] Lazy loading for parts. It also enables aliases. --- src/partcad/assembly.py | 3 --- src/partcad/part.py | 11 +++++++-- src/partcad/part_factory.py | 10 ++++++-- src/partcad/part_factory_build123d.py | 6 ++--- src/partcad/part_factory_cadquery.py | 6 ++--- src/partcad/part_factory_step.py | 7 +++--- src/partcad/project.py | 35 ++++++++++++--------------- src/partcad/shape.py | 2 +- tests/unit/test_part.py | 11 ++++++--- 9 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/partcad/assembly.py b/src/partcad/assembly.py index 2ef19df2..4d506cb7 100644 --- a/src/partcad/assembly.py +++ b/src/partcad/assembly.py @@ -81,9 +81,6 @@ def get_shape(self): self.shape = shape.wrapped return copy.copy(self.shape) - def get_wrapped(self): - return self.get_shape() - def _render_txt_real(self, file): for child in self.children: child._render_txt_real(file) diff --git a/src/partcad/part.py b/src/partcad/part.py index 505f3f50..30050fd4 100644 --- a/src/partcad/part.py +++ b/src/partcad/part.py @@ -17,15 +17,17 @@ class Part(shape.Shape): - def __init__(self, name=None, config={}, shape=None): + def __init__(self, name=None, path=None, config={}, shape=None): if name is None: name = "part" + "".join( random.choices(string.ascii_uppercase + string.digits, k=8) ) + if path is None: + path = name super().__init__(name) self.config = config - self.path = config["path"] + self.path = path self.shape = shape self.desc = None @@ -49,6 +51,11 @@ def __init__(self, name=None, config={}, shape=None): def set_shape(self, shape): self.shape = shape + def get_shape(self): + if self.shape is None: + self.instantiate(self) + return self.shape + def ref_inc(self): self.count += 1 diff --git a/src/partcad/part_factory.py b/src/partcad/part_factory.py index 3dfc3585..29bc9b35 100644 --- a/src/partcad/part_factory.py +++ b/src/partcad/part_factory.py @@ -16,6 +16,7 @@ class PartFactory: def __init__(self, ctx, project, part_config, extension=""): self.ctx = ctx self.project = project + self.part_config = part_config self.name = part_config["name"] self.path = self.name + extension @@ -34,7 +35,12 @@ def __init__(self, ctx, project, part_config, extension=""): part_config["path"] = self.path def _create(self, part_config): - self.part = p.Part(self.name, part_config) + self.part = p.Part(self.name, self.path, part_config) - def _save(self): self.project.parts[self.name] = self.part + if "aliases" in self.part_config and not self.part_config["aliases"] is None: + for alias in self.part_config["aliases"]: + # TODO(clairbee): test if this a copy or a reference + self.project.parts[alias] = self.part + + self.part.instantiate = lambda part_self: self.instantiate(part_self) diff --git a/src/partcad/part_factory_build123d.py b/src/partcad/part_factory_build123d.py index c23798af..c5c23b75 100644 --- a/src/partcad/part_factory_build123d.py +++ b/src/partcad/part_factory_build123d.py @@ -28,6 +28,7 @@ def __init__(self, ctx, project, part_config): # Complement the config object here if necessary self._create(part_config) + def instantiate(self, part): wrapper_path = wrapper.get("build123d.py") request = {"build_parameters": {}} @@ -45,8 +46,7 @@ def __init__(self, ctx, project, part_config): if result["success"]: shape = result["shape"] - self.part.shape = shape + part.set_shape(shape) else: logging.error(result["exception"]) - - self._save() + raise Exception(result["exception"]) diff --git a/src/partcad/part_factory_cadquery.py b/src/partcad/part_factory_cadquery.py index aa39b080..deeb3e6a 100644 --- a/src/partcad/part_factory_cadquery.py +++ b/src/partcad/part_factory_cadquery.py @@ -28,6 +28,7 @@ def __init__(self, ctx, project, part_config): # Complement the config object here if necessary self._create(part_config) + def instantiate(self, part): wrapper_path = wrapper.get("cadquery.py") request = {"build_parameters": {}} @@ -45,8 +46,7 @@ def __init__(self, ctx, project, part_config): if result["success"]: shape = result["shape"] - self.part.shape = shape + part.set_shape(shape) else: logging.error(result["exception"]) - - self._save() + raise Exception(result["exception"]) diff --git a/src/partcad/part_factory_step.py b/src/partcad/part_factory_step.py index 12a2b5e7..9043b1d2 100644 --- a/src/partcad/part_factory_step.py +++ b/src/partcad/part_factory_step.py @@ -18,7 +18,6 @@ def __init__(self, ctx, project, part_config): # Complement the config object here if necessary self._create(part_config) - shape = cq.importers.importStep(self.path).val().wrapped - self.part.set_shape(shape) - - self._save() + def instantiate(self, part): + shape = cq.importers.importStep(part.path).val().wrapped + part.set_shape(shape) diff --git a/src/partcad/project.py b/src/partcad/project.py index 7703efed..b58054bb 100644 --- a/src/partcad/project.py +++ b/src/partcad/project.py @@ -50,13 +50,16 @@ def __init__(self, ctx, path): else: self.desc = "" + self.init_parts() + self.init_assemblies() + def get_part_config(self, part_name): if not part_name in self.part_configs: return None return self.part_configs[part_name] - def get_part(self, part_name): - if not part_name in self.parts: + def init_parts(self): + for part_name in self.part_configs: part_config = self.get_part_config(part_name) # Handle the case of the part being declared in the config @@ -91,13 +94,10 @@ def get_part(self, part_name): ) return None - # Since factories do not return status codes, we need to verify - # whether they have produced the expected product or not - # TODO(clairbee): reconsider returning status from the factories - if not part_name in self.parts: - logging.error("Failed to instantiate the part: %s" % part_config) - return None - + def get_part(self, part_name): + if not part_name in self.parts: + logging.error("Part not found: %s" % part_name) + return None return self.parts[part_name] def get_assembly_config(self, assembly_name): @@ -105,8 +105,8 @@ def get_assembly_config(self, assembly_name): return None return self.assembly_configs[assembly_name] - def get_assembly(self, assembly_name): - if not assembly_name in self.assemblies: + def init_assemblies(self): + for assembly_name in self.assembly_configs: assembly_config = self.get_assembly_config(assembly_name) # Handle the case of the part being declared in the config @@ -133,15 +133,10 @@ def get_assembly(self, assembly_name): ) return None - # Since factories do not return status codes, we need to verify - # whether they have produced the expected product or not - # TODO(clairbee): reconsider returning status from the factories - if not assembly_name in self.assemblies: - logging.error( - "Failed to instantiate the assembly: %s" % assembly_config - ) - return None - + def get_assembly(self, assembly_name): + if not assembly_name in self.assemblies: + logging.error("Assembly not found: %s" % assembly_name) + return None return self.assemblies[assembly_name] def render(self, parts=None, assemblies=None): diff --git a/src/partcad/shape.py b/src/partcad/shape.py index fb0fbd16..661e8bc4 100644 --- a/src/partcad/shape.py +++ b/src/partcad/shape.py @@ -29,7 +29,7 @@ def __init__(self, name): self.svg_url = None def get_wrapped(self): - return self.shape + return self.get_shape() def get_cadquery(self) -> cq.Shape: cq_solid = cq.Solid.makeBox(1, 1, 1) diff --git a/tests/unit/test_part.py b/tests/unit/test_part.py index 47f67a1c..28c1d8eb 100644 --- a/tests/unit/test_part.py +++ b/tests/unit/test_part.py @@ -30,6 +30,7 @@ def test_part_get_1(): repo1 = ctx.get_project("this") bolt = repo1.get_part("bolt") assert bolt is not None + assert bolt.get_wrapped() is not None def test_part_get_2(): @@ -37,6 +38,7 @@ def test_part_get_2(): ctx = pc.Context("examples/part_step") bolt = ctx.get_part("bolt", "this") assert bolt is not None + assert bolt.get_wrapped() is not None def test_part_get_3(): @@ -45,6 +47,7 @@ def test_part_get_3(): _ = pc.ProjectFactoryLocal(ctx, None, test_config_local) cylinder = ctx.get_part("cylinder", "primitive_local") assert cylinder is not None + assert cylinder.get_wrapped() is not None # Note: The below test fails if there are braking changes in the way parts are @@ -57,6 +60,7 @@ def test_part_get_4(): assert factory.project.path.endswith(test_config_git["relPath"]) cube = factory.project.get_part("cube") assert cube is not None + assert cube.get_wrapped() is not None def test_part_lazy_loading_1(): @@ -64,9 +68,7 @@ def test_part_lazy_loading_1(): ctx = pc.Context() # Empty config _ = pc.ProjectFactoryLocal(ctx, None, test_config_local) cylinder = ctx.get_part("cylinder", "primitive_local") - # TODO(clairbee): implement lazy loading - # assert cylinder.shape is None - # logo.build() + assert cylinder.shape is None assert cylinder.get_wrapped() is not None @@ -77,6 +79,7 @@ def test_part_example_cadquery_primitive(): assert cube is not None cylinder = ctx.get_part("cylinder", "example_part_cadquery_primitive") assert cylinder is not None + assert cylinder.get_wrapped() is not None def test_part_example_cadquery_logo(): @@ -86,6 +89,7 @@ def test_part_example_cadquery_logo(): assert bone is not None head_half = ctx.get_part("head_half", "example_part_cadquery_logo") assert head_half is not None + assert head_half.get_wrapped() is not None def test_part_example_build123d_primitive(): @@ -93,3 +97,4 @@ def test_part_example_build123d_primitive(): ctx = pc.init("tests/partcad-examples.yaml") cube = ctx.get_part("cube", "example_part_build123d_primitive") assert cube is not None + assert cube.get_wrapped() is not None From 797c1e11e9212d59bdaf491081b0e2f2dab78572 Mon Sep 17 00:00:00 2001 From: Roman Kuzmenko Date: Sat, 6 Jan 2024 13:51:48 -0800 Subject: [PATCH 2/2] Added a test case for part aliases --- examples/part_cadquery_primitive/partcad.yaml | 1 + tests/unit/test_part.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/part_cadquery_primitive/partcad.yaml b/examples/part_cadquery_primitive/partcad.yaml index 3b2648f6..94b92b62 100644 --- a/examples/part_cadquery_primitive/partcad.yaml +++ b/examples/part_cadquery_primitive/partcad.yaml @@ -3,6 +3,7 @@ parts: cube: type: cadquery desc: This is a cube from examples + aliases: ["box"] cylinder: type: cadquery path: cylinder.py diff --git a/tests/unit/test_part.py b/tests/unit/test_part.py index 28c1d8eb..438920c5 100644 --- a/tests/unit/test_part.py +++ b/tests/unit/test_part.py @@ -55,7 +55,7 @@ def test_part_get_3(): # force pushed. def test_part_get_4(): """Instantiate a project by a git import config and load a part""" - ctx = pc.Context() # Emplty config + ctx = pc.Context() # Empty config factory = pc.ProjectFactoryGit(ctx, None, test_config_git) assert factory.project.path.endswith(test_config_git["relPath"]) cube = factory.project.get_part("cube") @@ -63,8 +63,8 @@ def test_part_get_4(): assert cube.get_wrapped() is not None -def test_part_lazy_loading_1(): - """Future test for lazy loading of geometry data""" +def test_part_lazy_loading(): + """Test for lazy loading of geometry data""" ctx = pc.Context() # Empty config _ = pc.ProjectFactoryLocal(ctx, None, test_config_local) cylinder = ctx.get_part("cylinder", "primitive_local") @@ -72,6 +72,16 @@ def test_part_lazy_loading_1(): assert cylinder.get_wrapped() is not None +def test_part_aliases(): + """Test for part aliases""" + ctx = pc.Context() # Empty config + _ = pc.ProjectFactoryLocal(ctx, None, test_config_local) + # "box" is an alias for "cube" + box = ctx.get_part("box", "primitive_local") + assert box.shape is None + assert box.get_wrapped() is not None + + def test_part_example_cadquery_primitive(): """Instantiate all parts from the example: part_cadquery_primitive""" ctx = pc.init("tests/partcad-examples.yaml")