""" The NHF build system Usage: For any parametric assembly, inherit the `Model` class, and mark the output objects with the `@target` decorator. Each marked function should only take `self` as an argument. ```python class BuildScaffold(Model): @target(name="obj1") def o1(self): return Cq.Solid.makeBox(10, 10, 10) def o2(self): return Cq.Solid.makeCylinder(10, 20) ``` """ from enum import Enum from pathlib import Path from typing import Union from functools import wraps from colorama import Fore, Style import cadquery as Cq import nhf.checks as NC class TargetKind(Enum): STL = "stl", DXF = "dxf", def __init__(self, ext: str): self.ext = ext class Target: """ Marks a function's output for serialization """ def __init__(self, method, name: str, kind: TargetKind = TargetKind.STL, **kwargs): self._method = method self.name = name self.kind = kind self.kwargs = kwargs def __str__(self): return f"" def __call__(self, obj, *args, **kwargs): """ Raw call function which passes arguments directly to `_method` """ return self._method(obj, *args, **kwargs) @property def file_name(self): """ Output file name """ return f"{self.name}.{self.kind.ext}" def write_to(self, obj, path: str): x = self._method(obj) if self.kind == TargetKind.STL: assert isinstance(x, Union[ Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly]) if isinstance(x, Cq.Workplane): x = x.val() if isinstance(x, Cq.Assembly): x = x.toCompound() x.exportStl(path, **self.kwargs) elif self.kind == TargetKind.DXF: assert isinstance(x, Cq.Workplane) Cq.exporters.exportDXF(x, path, **self.kwargs) else: assert False, f"Invalid kind: {self.kind}" @classmethod def methods(cls, subject): """ List of all methods of a class or objects annotated with this decorator. """ def g(): for name in dir(subject): if name == 'target_names': continue method = getattr(subject, name) if hasattr(method, '_target'): yield method._target return {method.name: method for method in g()} def target(name, **deco_kwargs): """ Decorator for annotating a build output """ def f(method): @wraps(method) def wrapper(self, *args, **kwargs): return method(self, *args, **kwargs) wrapper._target = Target(method, name, **deco_kwargs) return wrapper return f class Assembly: """ Marks a function's output for assembly property checking """ def __init__(self, method, collision_check: bool = True, **kwargs): self._method = method self.name = method.__name__ self.collision_check = collision_check self.kwargs = kwargs def __str__(self): return f"" def __call__(self, obj, *args, **kwargs): """ Raw call function which passes arguments directly to `_method` """ return self._method(obj, *args, **kwargs) def check(self, obj): x = self._method(obj) assert isinstance(x, Cq.Assembly) if self.collision_check: intersections = NC.pairwise_intersection(x) assert not intersections, f"In {self}, collision detected: {intersections}" @classmethod def methods(cls, subject): """ List of all methods of a class or objects annotated with this decorator. """ def g(): for name in dir(subject): if name == 'target_names': continue method = getattr(subject, name) if hasattr(method, '_assembly'): yield method._assembly return {method.name: method for method in g()} def assembly(**deco_kwargs): """ Decorator for annotating an assembly output """ def f(method): @wraps(method) def wrapper(self, *args, **kwargs): return method(self, *args, **kwargs) wrapper._assembly = Assembly(method, **deco_kwargs) return wrapper return f class Model: """ Base class for a parametric assembly """ def __init__(self, name: str): self.name = name @property def target_names(self) -> list[str]: """ List of decorated target functions """ return list(Target.methods(self).keys()) def check_all(self) -> int: """ Build all models and run all the checks. Return number of checks passed """ total = 0 for t in Target.methods(self).values(): t(self) total += 1 for t in Assembly.methods(self).values(): t.check(self) total += 1 return total def build_all(self, output_dir: Union[Path, str] = "build", verbose=1): """ Build all targets in this model and write the results to file """ output_dir = Path(output_dir) for t in Target.methods(self).values(): output_file = output_dir / self.name / t.file_name if output_file.is_file(): if verbose >= 1: print(f"{Fore.GREEN}Skipping{Style.RESET_ALL} {output_file}") continue output_file.parent.mkdir(exist_ok=True, parents=True) if verbose >= 1: print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}") try: t.write_to(self, str(output_file)) if verbose >= 1: print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}") except Exception as e: print(f"{Fore.RED}Failed to build{Style.RESET_ALL} {output_file}: {e}")