Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assembly support #440

Merged
merged 72 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
f879254
Initial version of the assembly API
adam-urbanczyk Aug 17, 2020
f08d9ad
Added dummy solver class
adam-urbanczyk Aug 18, 2020
16c3d28
Implemented cost function
adam-urbanczyk Aug 19, 2020
6f5ed87
Add objects to the Constraint type
adam-urbanczyk Aug 19, 2020
1efbef7
Dummy __init__
adam-urbanczyk Aug 19, 2020
39e247a
Started implementing assy export
adam-urbanczyk Aug 24, 2020
458c1ea
Ignore missing scipy stubs
adam-urbanczyk Aug 24, 2020
d4f443d
Another Location constructor overload
adam-urbanczyk Aug 24, 2020
ba55fd1
Disable mypy check for numpy and nptyping
adam-urbanczyk Aug 24, 2020
07e0de9
Initialize children
adam-urbanczyk Aug 26, 2020
52097fc
Reworked exportAssembly
adam-urbanczyk Aug 26, 2020
0c19653
Reorganized assemblies
adam-urbanczyk Aug 31, 2020
63bf354
Added export to native CAF format
adam-urbanczyk Sep 1, 2020
af5f213
Rewokred OCAF structure generation
adam-urbanczyk Sep 1, 2020
46f9011
Added tests
adam-urbanczyk Sep 3, 2020
834e230
Simplify setName
adam-urbanczyk Sep 4, 2020
40a214e
Better interpretable assy test
adam-urbanczyk Sep 4, 2020
cc4dd03
Test additional Loc ctor
adam-urbanczyk Sep 4, 2020
2029e40
Implemented save
adam-urbanczyk Sep 4, 2020
9693f5e
Better format definition
adam-urbanczyk Sep 4, 2020
5889920
Test assembly.save
adam-urbanczyk Sep 4, 2020
fbf6a54
Fixed format mismatch
adam-urbanczyk Sep 4, 2020
6c2840a
Py3.6 fix
adam-urbanczyk Sep 4, 2020
1fe46ba
Make the Assy API fluent
adam-urbanczyk Sep 4, 2020
726616b
Added basic docs
adam-urbanczyk Sep 4, 2020
4c9ed42
Added docstrings
adam-urbanczyk Sep 4, 2020
fe85050
Change add interface to be more generic
adam-urbanczyk Sep 4, 2020
60a6648
Color implementation for Assembly
adam-urbanczyk Sep 6, 2020
f71f7c5
docstring for Color
adam-urbanczyk Sep 6, 2020
10b885c
Additional docstrings
adam-urbanczyk Sep 7, 2020
e1bedca
fixed exportCAF file overwriting
adam-urbanczyk Sep 7, 2020
7e64565
Dummy docstring to trigger sphinx inclusion
adam-urbanczyk Sep 7, 2020
e3caf7b
Make cq_directive sphinx 3 compatibile
adam-urbanczyk Sep 7, 2020
3dc54d6
Use sphinx_autodoc_typehints
adam-urbanczyk Sep 7, 2020
7ab3d10
Merge branch 'master' into assembly
adam-urbanczyk Sep 7, 2020
e437f97
Initial implementation of constraint adding
adam-urbanczyk Sep 7, 2020
d185e03
Minor docstring fix
adam-urbanczyk Sep 7, 2020
cbe5a74
Import Constraint in the main namespace
adam-urbanczyk Sep 7, 2020
39c7640
Added optional constraint param
adam-urbanczyk Sep 7, 2020
ef9f5af
Doc tweaks
adam-urbanczyk Sep 7, 2020
3e57e3a
Constraint refactoring
adam-urbanczyk Sep 8, 2020
8fc43ca
Pin black version for now
adam-urbanczyk Sep 9, 2020
f53bd44
Working colors in STEP export
adam-urbanczyk Sep 9, 2020
c8c21d7
Black fix
adam-urbanczyk Sep 9, 2020
1963a65
Implemented init
adam-urbanczyk Sep 9, 2020
c15c364
Implemented constraint translation to the POD format
adam-urbanczyk Sep 9, 2020
46dd820
Added mising property
adam-urbanczyk Sep 9, 2020
146bbe5
Add nptyping to req
adam-urbanczyk Sep 9, 2020
e8cba7d
Add scipy to the specs
adam-urbanczyk Sep 9, 2020
dd863bd
Added test for normal
adam-urbanczyk Sep 11, 2020
f914815
Fixed normal test
adam-urbanczyk Sep 11, 2020
c5676b6
First pass on solve()
adam-urbanczyk Sep 13, 2020
4029701
Merge branch 'assembly' of https://github.com/CadQuery/cadquery into …
adam-urbanczyk Sep 13, 2020
c3432b6
Added tests for constrain and solve
adam-urbanczyk Sep 13, 2020
577139b
Fixed failures for constrain
adam-urbanczyk Sep 13, 2020
9c29c32
Fixed solve issues
adam-urbanczyk Sep 13, 2020
2cad052
Reworked the solver
adam-urbanczyk Sep 15, 2020
824a635
Different subloc handling
adam-urbanczyk Sep 16, 2020
17c8f1d
Simplified the solver and switched to BFGS
adam-urbanczyk Sep 16, 2020
e01b241
Another solver tweak
adam-urbanczyk Sep 21, 2020
f4150b6
Major rework of the solver
adam-urbanczyk Sep 24, 2020
e3cb80c
Tighter ftol
adam-urbanczyk Sep 25, 2020
b648495
Better assy.solve() test
adam-urbanczyk Sep 25, 2020
a24d6bf
Relaxed test criteria
adam-urbanczyk Sep 25, 2020
90ce94e
Changed the cost to use Angle nad added LSQ cost for experiments
adam-urbanczyk Sep 26, 2020
2625e21
Tighter tolerances on the solve
adam-urbanczyk Sep 26, 2020
012b1d6
Parameter handling
adam-urbanczyk Sep 29, 2020
43e8377
Docs update
adam-urbanczyk Sep 30, 2020
a170cb0
ftol --> gtol
adam-urbanczyk Sep 30, 2020
ba7830d
Do not touch the original parent
adam-urbanczyk Sep 30, 2020
8f62fc5
Review fixes
adam-urbanczyk Oct 1, 2020
21f7f48
Mention assemblies in the README
adam-urbanczyk Oct 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cadquery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
Selector,
)
from .cq import CQ, Workplane
from .assembly import Assembly
from . import selectors
from . import plugins


__all__ = [
"CQ",
"Workplane",
"Assembly",
"plugins",
"selectors",
"Plane",
Expand Down Expand Up @@ -64,4 +66,4 @@
"plugins",
]

__version__ = "2.0"
__version__ = "2.1dev"
202 changes: 202 additions & 0 deletions cadquery/assembly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
from typing import Union, Optional, List, Mapping, Any, overload, Tuple, Iterator, cast
from typing_extensions import Literal
from uuid import uuid1 as uuid

from .cq import Workplane
from .occ_impl.shapes import Shape
from .occ_impl.geom import Location
from .occ_impl.exporters.assembly import exportAssembly, exportCAF


AssemblyObjects = Union[Shape, Workplane, None]
ConstraintKinds = Literal["Plane", "Point", "Axis"]
ExportLiterals = Literal["STEP", "XML"]


class Constraint(object):

objects: Tuple[Shape, ...]
args: Tuple[Shape, ...]
kind: ConstraintKinds


class Assembly(object):
"""Nested assembly of Workplane and Shape objects defining their relative positions.
"""

loc: Location
name: str
metadata: Mapping[str, Any]

obj: AssemblyObjects
parent: Optional["Assembly"]
children: List["Assembly"]

objects: Mapping[str, AssemblyObjects]
constraints: List[Constraint]

def __init__(
self,
obj: AssemblyObjects = None,
loc: Optional[Location] = None,
name: Optional[str] = None,
):
"""
construct an assembly

:param obj: root object of the assembly (deafault: None)
:param loc: location of the root object (deafault: None, interpreted as identity transformation)
:param name: unique name of the root object (default: None, reasulting in an UUID being generated)
:return: An Assembly object.

To create an empt assembly use::

assy = Assembly(None)

To create one containt a root object::

b = Workplane().box(1,1,1)
assy = Assembly(b, Location(Vector(0,0,1)), name="root")

"""

self.obj = obj
self.loc = loc if loc else Location()
self.name = name if name else str(uuid())
self.parent = None

self.children = []
self.objects = {self.name: self.obj}

@overload
def add(
self,
obj: "Assembly",
loc: Optional[Location] = None,
name: Optional[str] = None,
) -> "Assembly":
"""
add a subassembly to the current assembly.

:param obj: subassembly to be added
:param loc: location of the root object (deafault: None, resulting in the location stored in the subassembly being used)
:param name: unique name of the root object (default: None, resulting in the name stored in the subassembly being used)
"""
...

@overload
def add(
self,
obj: AssemblyObjects,
loc: Optional[Location] = None,
name: Optional[str] = None,
) -> "Assembly":
"""
add a subassembly to the current assembly with explicit location and name

:param obj: object to be added as a subassembly
:param loc: location of the root object (deafault: None, interpreted as identity transformation)
:param name: unique name of the root object (default: None, resulting in an UUID being generated)
"""
...

def add(self, arg, **kwargs):

if isinstance(arg, Assembly):

subassy = Assembly(
arg.obj,
kwargs["loc"] if kwargs.get("loc") else arg.loc,
kwargs["name"] if kwargs.get("name") else arg.name,
)

subassy.children.extend(arg.children)
subassy.objects[subassy.name] = subassy.obj
subassy.objects.update(arg.objects)

self.children.append(subassy)
self.objects[subassy.name] = subassy.obj
self.objects.update(subassy.objects)

arg.parent = self

else:
assy = Assembly(arg, **kwargs)
assy.parent = self

self.add(assy)

return self

@overload
def constrain(self, query: str) -> "Assembly":
...

@overload
def constrain(self, q1: str, q2: str, kind: ConstraintKinds) -> "Assembly":
...

@overload
def constrain(
self, id1: str, s1: Shape, id2: str, s2: Shape, kind: ConstraintKinds
) -> "Assembly":
...

def constrain(self, *args):

raise NotImplementedError

def solve(self) -> "Assembly":

raise NotImplementedError

def save(
self, path: str, exportType: Optional[ExportLiterals] = None
) -> "Assembly":
"""
save as STEP or OCCT native XML file

:param path: filepath
:param exportType: export format (deafault: None, results in format being inferred form the path)
"""

if exportType is None:
t = path.split(".")[-1].upper()
if t in ("STEP", "XML"):
exportType = cast(ExportLiterals, t)
else:
raise ValueError("Unknown extension, specify export type explicitly")

if exportType == "STEP":
exportAssembly(self, path)
elif exportType == "XML":
exportCAF(self, path)
else:
raise ValueError(f"Unknown format: {exportType}")

return self

@classmethod
def load(cls, path: str) -> "Assembly":

raise NotImplementedError

@property
def shapes(self) -> List[Shape]:

rv: List[Shape] = []

if isinstance(self.obj, Shape):
rv = [self.obj]
elif isinstance(self.obj, Workplane):
rv = [el for el in self.obj.vals() if isinstance(el, Shape)]

return rv

def traverse(self) -> Iterator[Tuple[str, "Assembly"]]:

for ch in self.children:
for el in ch.traverse():
yield el

yield (self.name, self)
75 changes: 75 additions & 0 deletions cadquery/occ_impl/assembly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Iterable, Tuple, Dict
from typing_extensions import Protocol

from OCP.TDocStd import TDocStd_Document
from OCP.TCollection import TCollection_ExtendedString
from OCP.XCAFDoc import XCAFDoc_DocumentTool
from OCP.TDataStd import TDataStd_Name
from OCP.TDF import TDF_Label
from OCP.TopLoc import TopLoc_Location

from .geom import Location
from .shapes import Shape, Compound


class AssemblyProtocol(Protocol):
@property
def loc(self) -> Location:
...

@property
def name(self) -> str:
...

@property
def shapes(self) -> Iterable[Shape]:
...

@property
def children(self) -> Iterable["AssemblyProtocol"]:
...

def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]:
...


def setName(l: TDF_Label, name, tool):

TDataStd_Name.Set_s(l, TCollection_ExtendedString(name))


def toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]:

# prepare a doc
doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf"))
tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())
tool.SetAutoNaming_s(False)

# add root
top = tool.NewShape()
TDataStd_Name.Set_s(top, TCollection_ExtendedString("CQ assembly"))

# add leafs and subassemblies
subassys: Dict[str, Tuple[TDF_Label, Location]] = {}
for k, v in assy.traverse():
# leaf part
lab = tool.NewShape()
tool.SetShape(lab, Compound.makeCompound(v.shapes).wrapped)
setName(lab, f"{k}_part", tool)

# assy part
subassy = tool.NewShape()
tool.AddComponent(subassy, lab, TopLoc_Location())
setName(subassy, k, tool)

subassys[k] = (subassy, v.loc)

for ch in v.children:
tool.AddComponent(
subassy, subassys[ch.name][0], subassys[ch.name][1].wrapped
)

tool.AddComponent(top, subassys[assy.name][0], assy.loc.wrapped)
tool.UpdateAssemblies()

return top, doc
52 changes: 52 additions & 0 deletions cadquery/occ_impl/exporters/assembly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from OCP.XSControl import XSControl_WorkSession
from OCP.STEPCAFControl import STEPCAFControl_Writer
from OCP.STEPControl import STEPControl_StepModelType
from OCP.IFSelect import IFSelect_ReturnStatus
from OCP.XCAFApp import XCAFApp_Application
from OCP.XmlDrivers import (
XmlDrivers_DocumentStorageDriver,
XmlDrivers_DocumentRetrievalDriver,
)
from OCP.TCollection import TCollection_ExtendedString, TCollection_AsciiString
from OCP.PCDM import PCDM_StoreStatus

from ..assembly import AssemblyProtocol, toCAF


def exportAssembly(assy: AssemblyProtocol, path: str) -> bool:

_, doc = toCAF(assy)

session = XSControl_WorkSession()
writer = STEPCAFControl_Writer(session, False)
writer.SetNameMode(True)
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)

status = writer.Write(path)

return status == IFSelect_ReturnStatus.IFSelect_RetDone


def exportCAF(assy: AssemblyProtocol, path: str) -> bool:

_, doc = toCAF(assy)
app = XCAFApp_Application.GetApplication_s()

store = XmlDrivers_DocumentStorageDriver(
TCollection_ExtendedString("Copyright: Open Cascade, 2001-2002")
)
ret = XmlDrivers_DocumentRetrievalDriver()

app.DefineFormat(
TCollection_AsciiString("XmlOcaf"),
TCollection_AsciiString("Xml XCAF Document"),
TCollection_AsciiString(path.split(".")[-1]),
ret,
store,
)

status = app.SaveAs(doc, TCollection_ExtendedString(path))

app.Close(doc)

return status == PCDM_StoreStatus.PCDM_SS_OK
9 changes: 8 additions & 1 deletion cadquery/occ_impl/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,7 @@ def isInside(self, b2: "BoundBox") -> bool:

class Location(object):
"""Location in 3D space. Depending on usage can be absolute or relative.

This class wraps the TopLoc_Location class from OCCT. It can be used to move Shape
objects in both relative and absolute manner. It is the preferred type to locate objects
in CQ.
Expand Down Expand Up @@ -895,6 +895,11 @@ def __init__(self, t: TopLoc_Location) -> None:
"""Location wrapping the low-level TopLoc_Location object t"""
...

@overload
def __init__(self, t: gp_Trsf) -> None:
"""Location wrapping the low-level gp_Trsf object t"""
...

@overload
def __init__(self, t: Vector, ax: Vector, angle: float) -> None:
"""Location with translation t and rotation around ax by angle
Expand All @@ -919,6 +924,8 @@ def __init__(self, *args):
elif isinstance(t, TopLoc_Location):
self.wrapped = t
return
elif isinstance(t, gp_Trsf):
T = t
elif len(args) == 2:
t, v = args
cs = gp_Ax3(v.toPnt(), t.zDir.toDir(), t.xDir.toDir())
Expand Down
Loading