Cosplay/nhf/build.py

213 lines
6.2 KiB
Python
Raw Normal View History

2024-07-03 23:15:39 -07:00
"""
The NHF build system
Usage: For any parametric assembly, inherit the `Model` class, and mark the
2024-07-04 01:11:16 -07:00
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)
```
2024-07-03 23:15:39 -07:00
"""
2024-07-04 01:11:16 -07:00
from enum import Enum
2024-07-03 23:15:39 -07:00
from pathlib import Path
from typing import Union
from functools import wraps
2024-07-03 23:15:39 -07:00
from colorama import Fore, Style
import cadquery as Cq
2024-07-07 21:45:10 -07:00
import nhf.checks as NC
2024-07-03 23:15:39 -07:00
2024-07-11 22:29:05 -07:00
TOL=1e-6
2024-07-04 01:11:16 -07:00
class TargetKind(Enum):
STL = "stl",
DXF = "dxf",
def __init__(self, ext: str):
self.ext = ext
2024-07-03 23:15:39 -07:00
class Target:
2024-07-07 21:45:10 -07:00
"""
Marks a function's output for serialization
"""
2024-07-03 23:15:39 -07:00
def __init__(self,
method,
2024-07-04 01:11:16 -07:00
name: str,
prototype: bool = False,
2024-07-04 01:11:16 -07:00
kind: TargetKind = TargetKind.STL,
**kwargs):
2024-07-03 23:15:39 -07:00
self._method = method
self.name = name
self.prototype = prototype
2024-07-04 01:11:16 -07:00
self.kind = kind
self.kwargs = kwargs
def __str__(self):
2024-07-04 10:02:58 -07:00
return f"<target {self.name}.{self.kind.ext} {self._method}>"
2024-07-03 23:15:39 -07:00
def __call__(self, obj, *args, **kwargs):
2024-07-04 01:11:16 -07:00
"""
Raw call function which passes arguments directly to `_method`
"""
2024-07-03 23:15:39 -07:00
return self._method(obj, *args, **kwargs)
2024-07-04 10:02:58 -07:00
@property
def file_name(self):
"""
Output file name
"""
return f"{self.name}.{self.kind.ext}"
2024-07-04 01:11:16 -07:00
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):
2024-07-11 22:29:05 -07:00
x = x.toCompound().fuse(tol=TOL)
2024-07-04 01:11:16 -07:00
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}"
2024-07-03 23:15:39 -07:00
@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):
2024-07-04 00:42:14 -07:00
if name == 'target_names':
continue
2024-07-03 23:15:39 -07:00
method = getattr(subject, name)
2024-07-04 00:42:14 -07:00
if hasattr(method, '_target'):
yield method._target
return {method.name: method for method in g()}
2024-07-03 23:15:39 -07:00
def target(name, **deco_kwargs):
2024-07-03 23:15:39 -07:00
"""
Decorator for annotating a build output
"""
def f(method):
@wraps(method)
def wrapper(self, *args, **kwargs):
return method(self, *args, **kwargs)
2024-07-04 00:42:14 -07:00
wrapper._target = Target(method, name, **deco_kwargs)
return wrapper
2024-07-03 23:15:39 -07:00
return f
2024-07-07 21:45:10 -07:00
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"<assembly {self.name} {self._method}>"
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
2024-07-03 23:15:39 -07:00
class Model:
"""
Base class for a parametric assembly
"""
2024-07-04 10:02:58 -07:00
def __init__(self, name: str):
self.name = name
2024-07-03 23:15:39 -07:00
2024-07-04 00:42:14 -07:00
@property
2024-07-03 23:15:39 -07:00
def target_names(self) -> list[str]:
"""
List of decorated target functions
"""
return list(Target.methods(self).keys())
2024-07-04 00:42:14 -07:00
def check_all(self) -> int:
2024-07-04 10:02:58 -07:00
"""
2024-07-07 21:45:10 -07:00
Build all models and run all the checks. Return number of checks passed
2024-07-04 10:02:58 -07:00
"""
2024-07-04 00:42:14 -07:00
total = 0
2024-07-04 10:02:58 -07:00
for t in Target.methods(self).values():
t(self)
2024-07-07 21:45:10 -07:00
total += 1
for t in Assembly.methods(self).values():
t.check(self)
2024-07-04 00:42:14 -07:00
total += 1
return total
2024-07-04 01:11:16 -07:00
def build_all(self, output_dir: Union[Path, str] = "build", verbose=1):
2024-07-03 23:15:39 -07:00
"""
2024-07-04 10:02:58 -07:00
Build all targets in this model and write the results to file
2024-07-03 23:15:39 -07:00
"""
output_dir = Path(output_dir)
2024-07-04 10:02:58 -07:00
for t in Target.methods(self).values():
output_file = output_dir / self.name / t.file_name
2024-07-03 23:15:39 -07:00
if output_file.is_file():
if verbose >= 1:
print(f"{Fore.GREEN}Skipping{Style.RESET_ALL} {output_file}")
continue
2024-07-04 01:13:22 -07:00
output_file.parent.mkdir(exist_ok=True, parents=True)
2024-07-03 23:15:39 -07:00
if verbose >= 1:
print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}")
2024-07-04 10:02:58 -07:00
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}")