From f8792542401f5020809813c6d531f92d9d6aced4 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 17 Aug 2020 17:39:03 +0200 Subject: [PATCH 01/70] Initial version of the assembly API --- cadquery/assembly.py | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 cadquery/assembly.py diff --git a/cadquery/assembly.py b/cadquery/assembly.py new file mode 100644 index 000000000..1d437def4 --- /dev/null +++ b/cadquery/assembly.py @@ -0,0 +1,95 @@ +from typing import Union, Optional, List, Mapping, Any, overload, Tuple +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 + + +AssemblyObjects = Union[Shape, Workplane, None] +ConstraintKinds = Literal["Plane", "Point", "Axis"] + + +class Constraint(object): + + args: Tuple[Shape, ...] + kind: ConstraintKinds + + +class Assembly(object): + + 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, + loc: Optional[Location] = None, + name: Optional[str] = None, + ): + + self.obj = obj + self.loc = loc if loc else Location() + self.name = name if name else str(uuid()) + + self.objects = {self.name: self.obj} + + @overload + def add(self, obj: "Assembly"): + ... + + @overload + def add( + self, + obj: AssemblyObjects, + loc: Optional[Location] = None, + name: Optional[str] = None, + ): + ... + + def add(self, arg, **kwargs): + + if isinstance(arg, Assembly): + self.children.append(arg) + self.objects[arg.name] = arg.obj + self.objects.update(arg.objects) + + else: + self.add(Assembly(arg, **kwargs)) + + @overload + def constrain(self, query: str): + ... + + @overload + def constrain(self, q1: str, q2: str, kind: ConstraintKinds): + ... + + @overload + def constrain(self, s1: Shape, s2: Shape, kind: ConstraintKinds): + ... + + def constrain(self, *args): + + raise NotImplementedError + + def solve(self): + + raise NotImplementedError + + def save(self, path: str): + + raise NotImplementedError + + def load(self, path: str): + + raise NotImplementedError From f08d9adc6af06f9458309087ad18a97c5488b770 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 18 Aug 2020 17:24:34 +0200 Subject: [PATCH 02/70] Added dummy solver class --- cadquery/occ_impl/solver.py | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 cadquery/occ_impl/solver.py diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py new file mode 100644 index 000000000..5a161bf0d --- /dev/null +++ b/cadquery/occ_impl/solver.py @@ -0,0 +1,48 @@ +from typing import Tuple, Mapping, Union, Any, Callable, List +from nptyping import NDArray as Array + +from numpy import zeros, array +from scipy.optimize import least_squares + +from OCP.gp import gp_Dir, gp_Pnt + +from .geom import Location + +DOF6 = Tuple[float, float, float, float, float, float] +ConstraintMarker = Union[gp_Dir, gp_Pnt] + + +class ConstraintSolver(object): + + entities: Mapping[int, DOF6] + constraints: Mapping[ + Tuple[int, int], + Tuple[Tuple[ConstraintMarker, ...], Tuple[ConstraintMarker, ...]], + ] + + def _jacobianSparsity(self) -> Array[(Any, Any), float]: + + rv = zeros((len(self.constraints), 6 * len(self.entities))) + + for i, (k1, k2) in enumerate(self.constraints): + rv[i, 6 * k1 : 6 * (k1 + 1)] = 1 + rv[i, 6 * k2 : 6 * (k2 + 1)] = 1 + + return rv + + def _cost(self) -> Callable[[Array[(Any,), float]], Array[(Any,), float]]: + def f(x): + + rv = zeros(len(self.constraints)) + + return rv + + return f + + def solve(self) -> List[Location]: + + x0 = array([el for el in self.entities.values()]).ravel() + + res = least_squares(self._cost(), x0, jac_sparsity=self._jacobianSparsity()) + + return res.x From 16c3d28c7dea748a9310469db28d56ddcc153212 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 19 Aug 2020 16:38:57 +0200 Subject: [PATCH 03/70] Implemented cost function --- cadquery/occ_impl/solver.py | 47 +++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 5a161bf0d..62621666e 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -4,7 +4,7 @@ from numpy import zeros, array from scipy.optimize import least_squares -from OCP.gp import gp_Dir, gp_Pnt +from OCP.gp import gp_Vec, gp_Dir, gp_Pnt, gp_Trsf, gp_Quaternion from .geom import Location @@ -19,10 +19,12 @@ class ConstraintSolver(object): Tuple[int, int], Tuple[Tuple[ConstraintMarker, ...], Tuple[ConstraintMarker, ...]], ] + ne: int + nc: int def _jacobianSparsity(self) -> Array[(Any, Any), float]: - rv = zeros((len(self.constraints), 6 * len(self.entities))) + rv = zeros((self.nc, 6 * self.ne)) for i, (k1, k2) in enumerate(self.constraints): rv[i, 6 * k1 : 6 * (k1 + 1)] = 1 @@ -30,10 +32,43 @@ def _jacobianSparsity(self) -> Array[(Any, Any), float]: return rv + def _build_transform( + self, x: float, y: float, z: float, a: float, b: float, c: float + ) -> gp_Trsf: + + rv = gp_Trsf() + m = a ** 2 + b ** 2 + c ** 2 + + rv.SetTranslation(gp_Vec(x, y, z)) + rv.SetRotation( + gp_Quaternion(2 * a / m, 2 * b / m, 2 * c / m, (1 - m) / (m + 1)) + ) + + return rv + def _cost(self) -> Callable[[Array[(Any,), float]], Array[(Any,), float]]: def f(x): - rv = zeros(len(self.constraints)) + constraints = self.constraints + nc = self.nc + ne = self.ne + + rv = zeros(nc) + transforms = [ + self._build_transform(*x[6 * i : 6 * (i + 1)]) for i in range(ne) + ] + + for i, ((k1, k2), ms1, ms2) in enumerate(constraints.items()): + t1 = transforms[k1] + t2 = transforms[k2] + + for m1, m2 in zip(ms1, ms2): + if isinstance(m1, gp_Pnt): + rv[i] += (m1.Transformed(t1) - m2.Transformed(t2)).Magnitude() + elif isinstance(m1, gp_Dir): + rv[i] += m1.Transformed(t1) * m2.Transformed(t2) + else: + raise NotImplementedError return rv @@ -44,5 +79,9 @@ def solve(self) -> List[Location]: x0 = array([el for el in self.entities.values()]).ravel() res = least_squares(self._cost(), x0, jac_sparsity=self._jacobianSparsity()) + x = res.x - return res.x + return [ + Location(self._build_transform(*x[6 * i : 6 * (i + 1)])) + for i in range(self.ne) + ] From 6f5ed87804ef2e9f56b86515e8344139ea6f7902 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 19 Aug 2020 17:38:57 +0200 Subject: [PATCH 04/70] Add objects to the Constraint type --- cadquery/assembly.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 1d437def4..5f95857c3 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -13,6 +13,7 @@ class Constraint(object): + objects: Tuple[Shape, ...] args: Tuple[Shape, ...] kind: ConstraintKinds From 1efbef755a2e15c0ecd173e6b9f109aed0b413ca Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 19 Aug 2020 17:39:34 +0200 Subject: [PATCH 05/70] Dummy __init__ --- cadquery/occ_impl/solver.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 62621666e..b99d08d9a 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -10,18 +10,24 @@ DOF6 = Tuple[float, float, float, float, float, float] ConstraintMarker = Union[gp_Dir, gp_Pnt] +Constraint = Tuple[Tuple[ConstraintMarker, ...], Tuple[ConstraintMarker, ...]] class ConstraintSolver(object): entities: Mapping[int, DOF6] - constraints: Mapping[ - Tuple[int, int], - Tuple[Tuple[ConstraintMarker, ...], Tuple[ConstraintMarker, ...]], - ] + constraints: Mapping[Tuple[int, int], Constraint] ne: int nc: int + def __init__( + self, + entities: List[Location], + constraints: Mapping[Tuple[int, int], Constraint], + ): + + pass + def _jacobianSparsity(self) -> Array[(Any, Any), float]: rv = zeros((self.nc, 6 * self.ne)) From 39e247a856efa4c1158fd8539942024f3c044ef0 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 24 Aug 2020 20:05:27 +0200 Subject: [PATCH 06/70] Started implementing assy export --- cadquery/assembly.py | 16 +++++- cadquery/occ_impl/exporters/assembly.py | 69 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 cadquery/occ_impl/exporters/assembly.py diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 5f95857c3..e2fd25b9d 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -76,7 +76,9 @@ def constrain(self, q1: str, q2: str, kind: ConstraintKinds): ... @overload - def constrain(self, s1: Shape, s2: Shape, kind: ConstraintKinds): + def constrain( + self, id1: str, s1: Shape, id2: str, s2: Shape, kind: ConstraintKinds + ): ... def constrain(self, *args): @@ -94,3 +96,15 @@ def save(self, path: str): def load(self, path: str): 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 diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py new file mode 100644 index 000000000..c6fe2b74f --- /dev/null +++ b/cadquery/occ_impl/exporters/assembly.py @@ -0,0 +1,69 @@ +from typing import Iterable +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.XSControl import XSControl_WorkSession +from OCP.STEPCAFControl import STEPCAFControl_Writer +from OCP.STEPControl import STEPControl_StepModelType +from OCP.IFSelect import IFSelect_ReturnStatus + +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 exportAssembly(assy: AssemblyProtocol, path) -> bool: + + # prepare a doc + doc = TDocStd_Document(TCollection_ExtendedString("CQ assy")) + tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) + + # add root + root = tool.NewShape() + TDataStd_Name.Set_s(root, TCollection_ExtendedString(assy.name)) + + if assy.shapes: + tool.SetShape(root, Compound.makeCompound(assy.shapes).moved(assy.loc).wrapped) + + def processChildren(parent, children): + + for ch in children: + ch_node = tool.AddComponent( + parent, Compound.makeCompound(ch.shapes).moved(ch.loc).wrapped + ) + TDataStd_Name.Set_s(ch_node, TCollection_ExtendedString(ch.name)) + + if ch.children: + processChildren(ch_node, ch.children) + + processChildren(root, assy.children) + + tool.UpdateAssemblies() + + session = XSControl_WorkSession() + writer = STEPCAFControl_Writer(session, False) + writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs) + + status = writer.Write(path) + + return status == IFSelect_ReturnStatus.IFSelect_RetDone From 458c1eadc7f23c18011e0ef9e81527f0b0090cae Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 24 Aug 2020 20:08:46 +0200 Subject: [PATCH 07/70] Ignore missing scipy stubs --- mypy.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index ef5416ed8..7c1e97af9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -ignore_missing_imports = False +ignore_missing_imports = False [mypy-ezdxf.*] ignore_missing_imports = True @@ -9,3 +9,6 @@ ignore_missing_imports = True [mypy-IPython.*] ignore_missing_imports = True + +[mypy-scipy.*] +ignore_missing_imports = True \ No newline at end of file From d4f443d9c6ce68a7b279d1bf4bd1acc88aa23ac1 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 24 Aug 2020 21:23:42 +0200 Subject: [PATCH 08/70] Another Location constructor overload --- cadquery/occ_impl/geom.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 53958ce7f..e3c335a8b 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -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. @@ -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 @@ -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()) From ba55fd1e29d31dffb77c47898fc85951a74964c6 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 24 Aug 2020 21:51:17 +0200 Subject: [PATCH 09/70] Disable mypy check for numpy and nptyping --- mypy.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mypy.ini b/mypy.ini index 7c1e97af9..e6ede69d8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -11,4 +11,10 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-scipy.*] +ignore_missing_imports = True + +[mypy-numpy.*] +ignore_missing_imports = True + +[mypy-nptyping.*] ignore_missing_imports = True \ No newline at end of file From 07e0de9f73df2741d55fead7e091d75db84cdfc6 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 26 Aug 2020 22:52:39 +0200 Subject: [PATCH 10/70] Initialize children --- cadquery/assembly.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index e2fd25b9d..c2da64cc5 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -42,6 +42,7 @@ def __init__( self.loc = loc if loc else Location() self.name = name if name else str(uuid()) + self.children = [] self.objects = {self.name: self.obj} @overload From 52097fcee34e43a21312f4dc139644d760c28e86 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 26 Aug 2020 22:55:46 +0200 Subject: [PATCH 11/70] Reworked exportAssembly --- cadquery/occ_impl/exporters/assembly.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index c6fe2b74f..17665ce65 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -9,6 +9,7 @@ from OCP.STEPCAFControl import STEPCAFControl_Writer from OCP.STEPControl import STEPControl_StepModelType from OCP.IFSelect import IFSelect_ReturnStatus +from OCP.TDF import TDF_Label from ..geom import Location from ..shapes import Shape, Compound @@ -39,18 +40,24 @@ def exportAssembly(assy: AssemblyProtocol, path) -> bool: tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) # add root - root = tool.NewShape() - TDataStd_Name.Set_s(root, TCollection_ExtendedString(assy.name)) + top = tool.NewShape() - if assy.shapes: - tool.SetShape(root, Compound.makeCompound(assy.shapes).moved(assy.loc).wrapped) + root = tool.AddComponent( + top, Compound.makeCompound(assy.shapes).moved(assy.loc).wrapped, True + ) + TDataStd_Name.Set_s(root, TCollection_ExtendedString(assy.name)) def processChildren(parent, children): + if tool.IsReference_s(parent): + parent_ref, parent = parent, TDF_Label() + tool.GetReferredShape_s(parent_ref, parent) + for ch in children: ch_node = tool.AddComponent( - parent, Compound.makeCompound(ch.shapes).moved(ch.loc).wrapped + parent, Compound.makeCompound(ch.shapes).moved(ch.loc).wrapped, True ) + tool.UpdateAssemblies() TDataStd_Name.Set_s(ch_node, TCollection_ExtendedString(ch.name)) if ch.children: @@ -62,6 +69,7 @@ def processChildren(parent, children): session = XSControl_WorkSession() writer = STEPCAFControl_Writer(session, False) + writer.SetNameMode(True) writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs) status = writer.Write(path) From 0c19653af1fc0b7524d441cb4958b0fe6ddbc3cf Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 31 Aug 2020 15:31:21 +0200 Subject: [PATCH 12/70] Reorganized assemblies --- cadquery/__init__.py | 4 +- cadquery/occ_impl/assembly.py | 67 +++++++++++++++++++++++++ cadquery/occ_impl/exporters/assembly.py | 63 ++--------------------- 3 files changed, 73 insertions(+), 61 deletions(-) create mode 100644 cadquery/occ_impl/assembly.py diff --git a/cadquery/__init__.py b/cadquery/__init__.py index 28810cbc3..52e574ae9 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -28,6 +28,7 @@ Selector, ) from .cq import CQ, Workplane +from .assembly import Assembly from . import selectors from . import plugins @@ -35,6 +36,7 @@ __all__ = [ "CQ", "Workplane", + "Assembly", "plugins", "selectors", "Plane", @@ -64,4 +66,4 @@ "plugins", ] -__version__ = "2.0" +__version__ = "2.1dev" diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py new file mode 100644 index 000000000..431905e6c --- /dev/null +++ b/cadquery/occ_impl/assembly.py @@ -0,0 +1,67 @@ +from typing import Iterable, Tuple +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 .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 toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]: + + # prepare a doc + doc = TDocStd_Document(TCollection_ExtendedString("XmlXCAF")) + tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) + tool.SetAutoNaming_s(False) + + # add root + top = tool.NewShape() + + root = tool.AddComponent( + top, Compound.makeCompound(assy.shapes).moved(assy.loc).wrapped, True + ) + tool.UpdateAssemblies() + TDataStd_Name.Set_s(root, TCollection_ExtendedString(assy.name)) + + def processChildren(parent, children): + + if tool.IsReference_s(parent): + parent_ref, parent = parent, TDF_Label() + tool.GetReferredShape_s(parent_ref, parent) + + for ch in children: + ch_node = tool.AddComponent( + parent, Compound.makeCompound(ch.shapes).moved(ch.loc).wrapped, True + ) + tool.UpdateAssemblies() + TDataStd_Name.Set_s(ch_node, TCollection_ExtendedString(ch.name)) + + if ch.children: + processChildren(ch_node, ch.children) + + processChildren(root, assy.children) + tool.UpdateAssemblies() + + return root, doc diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index 17665ce65..2060b7309 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -1,71 +1,14 @@ -from typing import Iterable -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.XSControl import XSControl_WorkSession from OCP.STEPCAFControl import STEPCAFControl_Writer from OCP.STEPControl import STEPControl_StepModelType from OCP.IFSelect import IFSelect_ReturnStatus -from OCP.TDF import TDF_Label - -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 exportAssembly(assy: AssemblyProtocol, path) -> bool: - - # prepare a doc - doc = TDocStd_Document(TCollection_ExtendedString("CQ assy")) - tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) - - # add root - top = tool.NewShape() - - root = tool.AddComponent( - top, Compound.makeCompound(assy.shapes).moved(assy.loc).wrapped, True - ) - TDataStd_Name.Set_s(root, TCollection_ExtendedString(assy.name)) - - def processChildren(parent, children): - - if tool.IsReference_s(parent): - parent_ref, parent = parent, TDF_Label() - tool.GetReferredShape_s(parent_ref, parent) - for ch in children: - ch_node = tool.AddComponent( - parent, Compound.makeCompound(ch.shapes).moved(ch.loc).wrapped, True - ) - tool.UpdateAssemblies() - TDataStd_Name.Set_s(ch_node, TCollection_ExtendedString(ch.name)) +from ..assembly import AssemblyProtocol, toCAF - if ch.children: - processChildren(ch_node, ch.children) - processChildren(root, assy.children) +def exportAssembly(assy: AssemblyProtocol, path: str) -> bool: - tool.UpdateAssemblies() + _, doc = toCAF(assy) session = XSControl_WorkSession() writer = STEPCAFControl_Writer(session, False) From 63bf354224c955ea8cf1454a2c7d9a97b0425bb2 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 1 Sep 2020 15:13:14 +0200 Subject: [PATCH 13/70] Added export to native CAF format --- cadquery/occ_impl/assembly.py | 21 ++++++++++++++++++--- cadquery/occ_impl/exporters/assembly.py | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 431905e6c..fc0fe8f9d 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -29,21 +29,35 @@ def children(self) -> Iterable["AssemblyProtocol"]: ... +def setName(l: TDF_Label, name, tool): + + origin = l + + if tool.IsReference_s(l): + origin = TDF_Label() + tool.GetReferredShape_s(l, origin) + else: + origin = l + + TDataStd_Name.Set_s(origin, TCollection_ExtendedString(name)) + + def toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]: # prepare a doc - doc = TDocStd_Document(TCollection_ExtendedString("XmlXCAF")) + 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")) root = tool.AddComponent( top, Compound.makeCompound(assy.shapes).moved(assy.loc).wrapped, True ) + setName(root, assy.name, tool) tool.UpdateAssemblies() - TDataStd_Name.Set_s(root, TCollection_ExtendedString(assy.name)) def processChildren(parent, children): @@ -55,8 +69,9 @@ def processChildren(parent, children): ch_node = tool.AddComponent( parent, Compound.makeCompound(ch.shapes).moved(ch.loc).wrapped, True ) + + setName(ch_node, ch.name, tool) tool.UpdateAssemblies() - TDataStd_Name.Set_s(ch_node, TCollection_ExtendedString(ch.name)) if ch.children: processChildren(ch_node, ch.children) diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index 2060b7309..98e453e2c 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -2,6 +2,10 @@ 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 +from OCP.TCollection import TCollection_ExtendedString +from OCP.PCDM import PCDM_StoreStatus from ..assembly import AssemblyProtocol, toCAF @@ -18,3 +22,16 @@ def exportAssembly(assy: AssemblyProtocol, path: str) -> bool: 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() + XmlDrivers.DefineFormat_s(app) + + status = app.SaveAs(doc, TCollection_ExtendedString(path)) + + app.Close(doc) + + return status == PCDM_StoreStatus.PCDM_SS_OK From af5f213fb86bd056e52e6d72946ac2e4ea0452a9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 1 Sep 2020 17:40:02 +0200 Subject: [PATCH 14/70] Rewokred OCAF structure generation --- cadquery/assembly.py | 10 +++++++- cadquery/occ_impl/assembly.py | 43 ++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index c2da64cc5..efe5ef9f2 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -1,4 +1,4 @@ -from typing import Union, Optional, List, Mapping, Any, overload, Tuple +from typing import Union, Optional, List, Mapping, Any, overload, Tuple, Iterator from typing_extensions import Literal from uuid import uuid1 as uuid @@ -109,3 +109,11 @@ def shapes(self) -> List[Shape]: 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) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index fc0fe8f9d..78397cf59 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -1,4 +1,4 @@ -from typing import Iterable, Tuple +from typing import Iterable, Tuple, Dict from typing_extensions import Protocol from OCP.TDocStd import TDocStd_Document @@ -6,6 +6,7 @@ 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 @@ -28,6 +29,9 @@ def shapes(self) -> Iterable[Shape]: def children(self) -> Iterable["AssemblyProtocol"]: ... + def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]: + ... + def setName(l: TDF_Label, name, tool): @@ -53,30 +57,27 @@ def toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]: top = tool.NewShape() TDataStd_Name.Set_s(top, TCollection_ExtendedString("CQ assembly")) - root = tool.AddComponent( - top, Compound.makeCompound(assy.shapes).moved(assy.loc).wrapped, True - ) - setName(root, assy.name, tool) - tool.UpdateAssemblies() + # 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) - def processChildren(parent, children): + # assy part + subassy = tool.NewShape() + tool.AddComponent(subassy, lab, TopLoc_Location()) + setName(subassy, k, tool) - if tool.IsReference_s(parent): - parent_ref, parent = parent, TDF_Label() - tool.GetReferredShape_s(parent_ref, parent) + subassys[k] = (subassy, v.loc) - for ch in children: - ch_node = tool.AddComponent( - parent, Compound.makeCompound(ch.shapes).moved(ch.loc).wrapped, True + for ch in v.children: + tool.AddComponent( + subassy, subassys[ch.name][0], subassys[ch.name][1].wrapped ) - setName(ch_node, ch.name, tool) - tool.UpdateAssemblies() - - if ch.children: - processChildren(ch_node, ch.children) - - processChildren(root, assy.children) + tool.AddComponent(top, subassys[assy.name][0], assy.loc.wrapped) tool.UpdateAssemblies() - return root, doc + return top, doc From 46f90110ef6dc290ffec0e907aff802c9cd8b943 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Sep 2020 21:54:19 +0200 Subject: [PATCH 15/70] Added tests --- tests/test_assembly.py | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_assembly.py diff --git a/tests/test_assembly.py b/tests/test_assembly.py new file mode 100644 index 000000000..bde86130d --- /dev/null +++ b/tests/test_assembly.py @@ -0,0 +1,74 @@ +import pytest +import os + +import cadquery as cq +from cadquery.occ_impl.exporters.assembly import exportAssembly, exportCAF + + +@pytest.fixture +def simple_assy(): + + b1 = cq.Workplane().box(1, 1, 1) + b2 = cq.Workplane().box(1, 1, 2) + b3 = cq.Workplane().pushPoints([(0, 0), (-2, -5)]).box(1, 1, 3) + + assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(2, -5, 0))) + assy.add(b2, loc=cq.Location(cq.Vector(1, 1, 0))) + assy.add(b3, loc=cq.Location(cq.Vector(2, 3, 0))) + + return assy + + +@pytest.fixture +def nested_assy(): + + b1 = cq.Workplane().box(1, 1, 1) + b2 = cq.Workplane().box(1, 1, 2) + b3 = cq.Workplane().pushPoints([(0, 0), (-2, -5)]).box(1, 1, 3) + + assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(2, -5, 0)), name="TOP") + assy2 = cq.Assembly(b2, loc=cq.Location(cq.Vector(2, 2, 0)), name="SECOND") + assy2.add(b3, loc=cq.Location(cq.Vector(0, -2, 0)), name="BOTTOM") + + assy.add(assy2) + + return assy + + +def test_assembly(simple_assy, nested_assy): + + # basic checks + assert len(simple_assy.objects) == 3 + assert len(simple_assy.children) == 2 + assert len(simple_assy.shapes) == 1 + + assert len(nested_assy.objects) == 3 + assert len(nested_assy.children) == 1 + + # bottm-up traversal + kvs = list(nested_assy.traverse()) + + assert kvs[0][0] == "BOTTOM" + assert len(kvs[0][1].shapes[0].Solids()) == 2 + assert kvs[-1][0] == "TOP" + + +def test_step_export(simple_assy): + + exportAssembly(simple_assy, "simple.step") + + w = cq.importers.importStep("simple.step") + assert w.solids().size() == 4 + + # check that locations were applied correctly + c = cq.Compound.makeCompound(w.solids().vals()).Center() + c.toTuple() + assert pytest.approx(c.toTuple()) == (2.888888888888889, -4.444444444444445, 0) + + +def test_native_export(simple_assy): + + exportCAF(simple_assy, "assy.xml") + + # only sanity check for now + assert os.path.exists("assy.xml") From 834e23052e2a6ba6b28e3fededf1ad9bd849f22b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 10:00:00 +0200 Subject: [PATCH 16/70] Simplify setName --- cadquery/occ_impl/assembly.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 78397cf59..2e8bd3e61 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -35,15 +35,7 @@ def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]: def setName(l: TDF_Label, name, tool): - origin = l - - if tool.IsReference_s(l): - origin = TDF_Label() - tool.GetReferredShape_s(l, origin) - else: - origin = l - - TDataStd_Name.Set_s(origin, TCollection_ExtendedString(name)) + TDataStd_Name.Set_s(l, TCollection_ExtendedString(name)) def toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]: From 40a214e467adb91d661e92412a6b12cae068ccba Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 10:17:53 +0200 Subject: [PATCH 17/70] Better interpretable assy test --- tests/test_assembly.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index bde86130d..699237317 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -8,7 +8,7 @@ @pytest.fixture def simple_assy(): - b1 = cq.Workplane().box(1, 1, 1) + b1 = cq.Solid.makeBox(1,1,1) b2 = cq.Workplane().box(1, 1, 2) b3 = cq.Workplane().pushPoints([(0, 0), (-2, -5)]).box(1, 1, 3) @@ -23,12 +23,12 @@ def simple_assy(): def nested_assy(): b1 = cq.Workplane().box(1, 1, 1) - b2 = cq.Workplane().box(1, 1, 2) - b3 = cq.Workplane().pushPoints([(0, 0), (-2, -5)]).box(1, 1, 3) + b2 = cq.Workplane().box(1, 1, 1) + b3 = cq.Workplane().pushPoints([(-2, 0), (2, 0)]).box(1, 1, .5) - assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(2, -5, 0)), name="TOP") - assy2 = cq.Assembly(b2, loc=cq.Location(cq.Vector(2, 2, 0)), name="SECOND") - assy2.add(b3, loc=cq.Location(cq.Vector(0, -2, 0)), name="BOTTOM") + assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(0, 0, 0)), name="TOP") + assy2 = cq.Assembly(b2, loc=cq.Location(cq.Vector(0, 4, 0)), name="SECOND") + assy2.add(b3, loc=cq.Location(cq.Vector(0, 4, 0)), name="BOTTOM") assy.add(assy2) @@ -53,17 +53,17 @@ def test_assembly(simple_assy, nested_assy): assert kvs[-1][0] == "TOP" -def test_step_export(simple_assy): +def test_step_export(nested_assy): - exportAssembly(simple_assy, "simple.step") + exportAssembly(nested_assy, "nested.step") - w = cq.importers.importStep("simple.step") + w = cq.importers.importStep("nested.step") assert w.solids().size() == 4 # check that locations were applied correctly c = cq.Compound.makeCompound(w.solids().vals()).Center() c.toTuple() - assert pytest.approx(c.toTuple()) == (2.888888888888889, -4.444444444444445, 0) + assert pytest.approx(c.toTuple()) == (0, 4, 0) def test_native_export(simple_assy): From cc4dd03e5455ff7c806d509ea21625807e36a5cf Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 10:18:11 +0200 Subject: [PATCH 18/70] Test additional Loc ctor --- tests/test_cad_objects.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_cad_objects.py b/tests/test_cad_objects.py index 91ccd2c6c..bfa2e22c2 100644 --- a/tests/test_cad_objects.py +++ b/tests/test_cad_objects.py @@ -2,7 +2,7 @@ import math import unittest from tests import BaseTest -from OCP.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_Elips, gp, gp_XYZ +from OCP.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_Elips, gp, gp_XYZ, gp_Trsf from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge from cadquery import * @@ -395,7 +395,7 @@ def testPlaneNotEqual(self): ) def testLocation(self): - + # Vector loc1 = Location(Vector(0, 0, 1)) @@ -408,6 +408,13 @@ def testLocation(self): angle = loc2.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG self.assertAlmostEqual(45, angle) + # gp_Trsf + T = gp_Trsf() + T.SetTranslation(gp_Vec(0,0,1)) + loc3 = Location(T) + + assert( loc1.wrapped.Transformation().TranslationPart().Z() == loc3.wrapped.Transformation().TranslationPart().Z()) + if __name__ == "__main__": unittest.main() From 2029e40a7bc9df045dd15ac399598a13e749a33a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 11:46:56 +0200 Subject: [PATCH 19/70] Implemented save --- cadquery/assembly.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index efe5ef9f2..31b5503bc 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -5,10 +5,12 @@ 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): @@ -90,9 +92,21 @@ def solve(self): raise NotImplementedError - def save(self, path: str): + def save(self, path: str, exportType: Optional[ExportLiterals] = None): - raise NotImplementedError + if exportType is None: + t = path.split(".")[-1].upper() + if t in ExportLiterals.__args__: + exportType = 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}") def load(self, path: str): From 9693f5e35797cfc524285bf35b3c6113d73a5753 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 11:48:15 +0200 Subject: [PATCH 20/70] Better format definition --- cadquery/occ_impl/exporters/assembly.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index 98e453e2c..99dd08f7f 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -3,8 +3,11 @@ from OCP.STEPControl import STEPControl_StepModelType from OCP.IFSelect import IFSelect_ReturnStatus from OCP.XCAFApp import XCAFApp_Application -from OCP.XmlDrivers import XmlDrivers -from OCP.TCollection import TCollection_ExtendedString +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 @@ -28,7 +31,19 @@ def exportCAF(assy: AssemblyProtocol, path: str) -> bool: _, doc = toCAF(assy) app = XCAFApp_Application.GetApplication_s() - XmlDrivers.DefineFormat_s(app) + + store = XmlDrivers_DocumentStorageDriver( + TCollection_ExtendedString("Copyright: Open Cascade, 2001-2002") + ) + ret = XmlDrivers_DocumentRetrievalDriver() + + app.DefineFormat( + TCollection_AsciiString("XmlXCAF"), + TCollection_AsciiString("Xml XCAF Document"), + TCollection_AsciiString(path.split(".")[-1]), + ret, + store, + ) status = app.SaveAs(doc, TCollection_ExtendedString(path)) From 58899208e00ee51d5f2e85b667d88e7e868bc1ae Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 11:48:28 +0200 Subject: [PATCH 21/70] Test assembly.save --- tests/test_assembly.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 699237317..fef61f2e5 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -34,7 +34,6 @@ def nested_assy(): return assy - def test_assembly(simple_assy, nested_assy): # basic checks @@ -72,3 +71,26 @@ def test_native_export(simple_assy): # only sanity check for now assert os.path.exists("assy.xml") + +def test_save(simple_assy): + + simple_assy.save('simple.step') + assert os.path.exists("simple.step") + + simple_assy.save('simple.xml') + assert os.path.exists("simple.xml") + + simple_assy.save('simple.step') + assert os.path.exists("simple.step") + + simple_assy.save('simple.stp','STEP') + assert os.path.exists("simple.stp") + + simple_assy.save('simple.caf','XML') + assert os.path.exists("simple.caf.xml") + + with pytest.raises(ValueError): + simple_assy.save('simple.dxf') + + with pytest.raises(ValueError): + simple_assy.save('simple.step','DXF') \ No newline at end of file From fbf6a54a0e9111ae7c8ab1ef4c0457d5890a2bda Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 11:53:26 +0200 Subject: [PATCH 22/70] Fixed format mismatch --- cadquery/occ_impl/exporters/assembly.py | 2 +- tests/test_assembly.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index 99dd08f7f..f07f091ef 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -38,7 +38,7 @@ def exportCAF(assy: AssemblyProtocol, path: str) -> bool: ret = XmlDrivers_DocumentRetrievalDriver() app.DefineFormat( - TCollection_AsciiString("XmlXCAF"), + TCollection_AsciiString("XmlOcaf"), TCollection_AsciiString("Xml XCAF Document"), TCollection_AsciiString(path.split(".")[-1]), ret, diff --git a/tests/test_assembly.py b/tests/test_assembly.py index fef61f2e5..39c9d7bca 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -87,7 +87,7 @@ def test_save(simple_assy): assert os.path.exists("simple.stp") simple_assy.save('simple.caf','XML') - assert os.path.exists("simple.caf.xml") + assert os.path.exists("simple.caf") with pytest.raises(ValueError): simple_assy.save('simple.dxf') From 6c2840a446b57069eaeb070368b98a79afac944d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 12:49:25 +0200 Subject: [PATCH 23/70] Py3.6 fix --- cadquery/assembly.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 31b5503bc..7fff01d76 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -1,4 +1,4 @@ -from typing import Union, Optional, List, Mapping, Any, overload, Tuple, Iterator +from typing import Union, Optional, List, Mapping, Any, overload, Tuple, Iterator, cast from typing_extensions import Literal from uuid import uuid1 as uuid @@ -96,8 +96,8 @@ def save(self, path: str, exportType: Optional[ExportLiterals] = None): if exportType is None: t = path.split(".")[-1].upper() - if t in ExportLiterals.__args__: - exportType = t + if t in ("STEP", "XML"): + exportType = cast(ExportLiterals, t) else: raise ValueError("Unknown extension, specify export type explicitly") From 1fe46ba177a8f168036ba107c6696bf54d59b390 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 13:07:47 +0200 Subject: [PATCH 24/70] Make the Assy API fluent --- cadquery/assembly.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 7fff01d76..2146ab749 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -48,7 +48,7 @@ def __init__( self.objects = {self.name: self.obj} @overload - def add(self, obj: "Assembly"): + def add(self, obj: "Assembly") -> "Assembly": ... @overload @@ -57,7 +57,7 @@ def add( obj: AssemblyObjects, loc: Optional[Location] = None, name: Optional[str] = None, - ): + ) -> "Assembly": ... def add(self, arg, **kwargs): @@ -70,29 +70,33 @@ def add(self, arg, **kwargs): else: self.add(Assembly(arg, **kwargs)) + return self + @overload - def constrain(self, query: str): + def constrain(self, query: str) -> "Assembly": ... @overload - def constrain(self, q1: str, q2: str, kind: ConstraintKinds): + 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): + def solve(self) -> "Assembly": raise NotImplementedError - def save(self, path: str, exportType: Optional[ExportLiterals] = None): + def save( + self, path: str, exportType: Optional[ExportLiterals] = None + ) -> "Assembly": if exportType is None: t = path.split(".")[-1].upper() @@ -108,7 +112,10 @@ def save(self, path: str, exportType: Optional[ExportLiterals] = None): else: raise ValueError(f"Unknown format: {exportType}") - def load(self, path: str): + return self + + @classmethod + def load(cls, path: str) -> "Assembly": raise NotImplementedError From 726616b582457c0be7bd0887a67e1a7c3ff0b799 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 13:11:16 +0200 Subject: [PATCH 25/70] Added basic docs --- doc/apireference.rst | 13 +++++++++++++ doc/classreference.rst | 2 ++ doc/primer.rst | 16 ++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/doc/apireference.rst b/doc/apireference.rst index 82a4ea905..d2a3035cc 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -185,3 +185,16 @@ as a basis for futher operations. SubtractSelector InverseSelector StringSyntaxSelector + +Assemblies +---------- + +Workplane and Shape objects can be connected together into assemblies + +.. currentmodule:: cadquery + +.. autosummary:: + + Assembly + Assembly.add + Assembly.save diff --git a/doc/classreference.rst b/doc/classreference.rst index ce88a2a78..58261c45d 100644 --- a/doc/classreference.rst +++ b/doc/classreference.rst @@ -18,6 +18,7 @@ Core Classes .. autosummary:: CQ Workplane + Assembly Topological Classes ---------------------- @@ -39,6 +40,7 @@ Geometry Classes Vector Matrix Plane + Location Selector Classes --------------------- diff --git a/doc/primer.rst b/doc/primer.rst index 41b14adfa..4c6c5875d 100644 --- a/doc/primer.rst +++ b/doc/primer.rst @@ -165,3 +165,19 @@ iterates on each member of the stack. This is really useful to remember when you author your own plugins. :py:meth:`cadquery.cq.Workplane.each` is useful for this purpose. +Assemblies +---------- + +Simple models can be combined into complex, possibly nested, assemblies:: + + part1 = Workplane().box(1,1,1) + part2 = Workplane().box(1,1,2) + part3 = Workplane().box(1,1,3) + + assy = ( + Assembly(part1, Location((1,0,0))) + .add(part2, Location(1,0,0)) + .add(part3, Location(-1,0,0)) + ) + +Note that the locations of the children parts are defined with respect to their parents - in the above example ``part3`` will be located at (0,0,0) in the global coordinate system. From 4c9ed42ffdaca31d389fe294f4d2c3023b79ff9c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 16:21:00 +0200 Subject: [PATCH 26/70] Added docstrings --- cadquery/assembly.py | 48 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 2146ab749..2827d02b0 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -21,6 +21,8 @@ class Constraint(object): class Assembly(object): + """Nested assembly of Workplane and Shape objects defining their relative positions. + """ loc: Location name: str @@ -35,20 +37,44 @@ class Assembly(object): def __init__( self, - obj: AssemblyObjects, + 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") -> "Assembly": + """ + add a subassembly to the current assembly. + + :param obj: subassembly to be added + """ ... @overload @@ -58,6 +84,13 @@ def add( 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, reasulting in an UUID being generated) + """ ... def add(self, arg, **kwargs): @@ -67,8 +100,13 @@ def add(self, arg, **kwargs): self.objects[arg.name] = arg.obj self.objects.update(arg.objects) + arg.parent = self + else: - self.add(Assembly(arg, **kwargs)) + assy = Assembly(arg, **kwargs) + assy.parent = self + + self.add(assy) return self @@ -97,6 +135,12 @@ def solve(self) -> "Assembly": 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() From fe850502f4d27ae1f004bce52bc3922a4b952c58 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 4 Sep 2020 16:43:06 +0200 Subject: [PATCH 27/70] Change add interface to be more generic --- cadquery/assembly.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 2827d02b0..6d9e2aa96 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -69,11 +69,18 @@ def __init__( self.objects = {self.name: self.obj} @overload - def add(self, obj: "Assembly") -> "Assembly": + 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) """ ... @@ -89,16 +96,27 @@ def add( :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, reasulting in an UUID being generated) + :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): - self.children.append(arg) - self.objects[arg.name] = arg.obj - self.objects.update(arg.objects) + + 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 From 60a664874fc09044583dad18c7a89abdfec6cd0c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Sep 2020 17:45:32 +0200 Subject: [PATCH 28/70] Color implementation for Assembly --- cadquery/__init__.py | 3 +- cadquery/assembly.py | 11 +++++ cadquery/occ_impl/assembly.py | 50 ++++++++++++++++++++-- cadquery/occ_impl/exporters/assembly.py | 2 + tests/test_assembly.py | 57 +++++++++++++++++-------- 5 files changed, 102 insertions(+), 21 deletions(-) diff --git a/cadquery/__init__.py b/cadquery/__init__.py index 52e574ae9..691f0326b 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -28,7 +28,7 @@ Selector, ) from .cq import CQ, Workplane -from .assembly import Assembly +from .assembly import Assembly, Color from . import selectors from . import plugins @@ -37,6 +37,7 @@ "CQ", "Workplane", "Assembly", + "Color", "plugins", "selectors", "Plane", diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 6d9e2aa96..7e3b5b0f4 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -5,6 +5,7 @@ from .cq import Workplane from .occ_impl.shapes import Shape from .occ_impl.geom import Location +from .occ_impl.assembly import Color from .occ_impl.exporters.assembly import exportAssembly, exportCAF @@ -26,6 +27,7 @@ class Assembly(object): loc: Location name: str + color: Optional[Color] metadata: Mapping[str, Any] obj: AssemblyObjects @@ -40,6 +42,7 @@ def __init__( obj: AssemblyObjects = None, loc: Optional[Location] = None, name: Optional[str] = None, + color: Optional[Color] = None, ): """ construct an assembly @@ -47,8 +50,10 @@ def __init__( :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) + :param color: color of the added object (default: None) :return: An Assembly object. + To create an empt assembly use:: assy = Assembly(None) @@ -63,6 +68,7 @@ def __init__( self.obj = obj self.loc = loc if loc else Location() self.name = name if name else str(uuid()) + self.color = color if color else None self.parent = None self.children = [] @@ -74,6 +80,7 @@ def add( obj: "Assembly", loc: Optional[Location] = None, name: Optional[str] = None, + color: Optional[Color] = None, ) -> "Assembly": """ add a subassembly to the current assembly. @@ -81,6 +88,7 @@ def add( :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) + :param color: color of the added object (default: None, resulting in the color stored in the subassembly being used) """ ... @@ -90,6 +98,7 @@ def add( obj: AssemblyObjects, loc: Optional[Location] = None, name: Optional[str] = None, + color: Optional[Color] = None, ) -> "Assembly": """ add a subassembly to the current assembly with explicit location and name @@ -97,6 +106,7 @@ def add( :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) + :param color: color of the added object (default: None) """ ... @@ -108,6 +118,7 @@ def add(self, arg, **kwargs): arg.obj, kwargs["loc"] if kwargs.get("loc") else arg.loc, kwargs["name"] if kwargs.get("name") else arg.name, + kwargs["color"] if kwargs.get("color") else arg.color, ) subassy.children.extend(arg.children) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 2e8bd3e61..c4e7fca33 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -1,17 +1,49 @@ -from typing import Iterable, Tuple, Dict +from typing import Iterable, Tuple, Dict, overload, Optional 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.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorType from OCP.TDataStd import TDataStd_Name from OCP.TDF import TDF_Label from OCP.TopLoc import TopLoc_Location +from OCP.Quantity import Quantity_ColorRGBA from .geom import Location from .shapes import Shape, Compound +class Color(object): + + wrapped: Quantity_ColorRGBA + + @overload + def __init__(self, name: str): + ... + + @overload + def __init__(self, r: float, g: float, b: float, a: float = 0): + ... + + def __init__(self, *args, **kwargs): + + if len(args) == 1: + self.wrapped = Quantity_ColorRGBA() + exists = Quantity_ColorRGBA.ColorFromName_s(args[0], self.wrapped) + if not exists: + raise ValueError(f"Unknown color name: {args[0]}") + elif len(args) == 3: + r, g, b = args + self.wrapped = Quantity_ColorRGBA(r, g, b, 1) + if kwargs.get("a"): + self.wrapped.SetAlpha(kwargs.get("a")) + elif len(args) == 4: + r, g, b, a = args + self.wrapped = Quantity_ColorRGBA(r, g, b, a) + else: + raise ValueError(f"Unsupported arguments: {args}, {kwargs}") + + class AssemblyProtocol(Protocol): @property def loc(self) -> Location: @@ -21,6 +53,10 @@ def loc(self) -> Location: def name(self) -> str: ... + @property + def color(self) -> Optional[Color]: + ... + @property def shapes(self) -> Iterable[Shape]: ... @@ -33,17 +69,23 @@ def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]: ... -def setName(l: TDF_Label, name, tool): +def setName(l: TDF_Label, name: str, tool): TDataStd_Name.Set_s(l, TCollection_ExtendedString(name)) +def setColor(l: TDF_Label, color: Color, tool): + + tool.SetColor(l, color.wrapped, XCAFDoc_ColorType.XCAFDoc_ColorSurf) + + 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) + ctool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main()) # add root top = tool.NewShape() @@ -61,6 +103,8 @@ def toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]: subassy = tool.NewShape() tool.AddComponent(subassy, lab, TopLoc_Location()) setName(subassy, k, tool) + if v.color: + setColor(subassy, v.color, ctool) subassys[k] = (subassy, v.loc) diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index f07f091ef..88a0ae9e4 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -20,6 +20,8 @@ def exportAssembly(assy: AssemblyProtocol, path: str) -> bool: session = XSControl_WorkSession() writer = STEPCAFControl_Writer(session, False) writer.SetNameMode(True) + writer.SetColorMode(True) + writer.SetLayerMode(True) writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs) status = writer.Write(path) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 39c9d7bca..d412d7409 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -8,7 +8,7 @@ @pytest.fixture def simple_assy(): - b1 = cq.Solid.makeBox(1,1,1) + b1 = cq.Solid.makeBox(1, 1, 1) b2 = cq.Workplane().box(1, 1, 2) b3 = cq.Workplane().pushPoints([(0, 0), (-2, -5)]).box(1, 1, 3) @@ -24,16 +24,38 @@ def nested_assy(): b1 = cq.Workplane().box(1, 1, 1) b2 = cq.Workplane().box(1, 1, 1) - b3 = cq.Workplane().pushPoints([(-2, 0), (2, 0)]).box(1, 1, .5) + b3 = cq.Workplane().pushPoints([(-2, 0), (2, 0)]).box(1, 1, 0.5) assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(0, 0, 0)), name="TOP") assy2 = cq.Assembly(b2, loc=cq.Location(cq.Vector(0, 4, 0)), name="SECOND") assy2.add(b3, loc=cq.Location(cq.Vector(0, 4, 0)), name="BOTTOM") - assy.add(assy2) + assy.add(assy2, color=cq.Color("green")) return assy + +def test_color(): + + c1 = cq.Color("red") + assert c1.wrapped.GetRGB().Red() == 1 + assert c1.wrapped.Alpha() == 1 + + c2 = cq.Color(1, 0, 0) + assert c2.wrapped.GetRGB().Red() == 1 + assert c2.wrapped.Alpha() == 1 + + c3 = cq.Color(1, 0, 0, 0.5) + assert c3.wrapped.GetRGB().Red() == 1 + assert c3.wrapped.Alpha() == 0.5 + + with pytest.raises(ValueError): + cq.Color("?????") + + with pytest.raises(ValueError): + cq.Color(1, 2, 3, 4, 5) + + def test_assembly(simple_assy, nested_assy): # basic checks @@ -72,25 +94,26 @@ def test_native_export(simple_assy): # only sanity check for now assert os.path.exists("assy.xml") + def test_save(simple_assy): - - simple_assy.save('simple.step') + + simple_assy.save("simple.step") assert os.path.exists("simple.step") - - simple_assy.save('simple.xml') + + simple_assy.save("simple.xml") assert os.path.exists("simple.xml") - - simple_assy.save('simple.step') + + simple_assy.save("simple.step") assert os.path.exists("simple.step") - - simple_assy.save('simple.stp','STEP') + + simple_assy.save("simple.stp", "STEP") assert os.path.exists("simple.stp") - - simple_assy.save('simple.caf','XML') + + simple_assy.save("simple.caf", "XML") assert os.path.exists("simple.caf") - + with pytest.raises(ValueError): - simple_assy.save('simple.dxf') - + simple_assy.save("simple.dxf") + with pytest.raises(ValueError): - simple_assy.save('simple.step','DXF') \ No newline at end of file + simple_assy.save("simple.step", "DXF") From f71f7c5173a03c74eeca3160793d295453d00d8b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 6 Sep 2020 19:50:21 +0200 Subject: [PATCH 29/70] docstring for Color --- cadquery/occ_impl/assembly.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index c4e7fca33..fe0b38fb0 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -19,10 +19,23 @@ class Color(object): @overload def __init__(self, name: str): + """ + Construct a Color from a name. + + :param name: name of the color, e.g. green + """ ... @overload def __init__(self, r: float, g: float, b: float, a: float = 0): + """ + Construct a Color from RGB(A) values. + + :param r: red value, 0-1 + :param g: green value, 0-1 + :param b: blue value, 0-1 + :param a: alpha value, 0-1 (default: 0) + """ ... def __init__(self, *args, **kwargs): From 10b885cbb5c077db7cc9c5082406bdc2addd9f10 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 10:21:13 +0200 Subject: [PATCH 30/70] Additional docstrings --- cadquery/assembly.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 7e3b5b0f4..dbec1a611 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -194,6 +194,9 @@ def load(cls, path: str) -> "Assembly": @property def shapes(self) -> List[Shape]: + """ + List of Shape objects in the .obj field + """ rv: List[Shape] = [] @@ -205,6 +208,9 @@ def shapes(self) -> List[Shape]: return rv def traverse(self) -> Iterator[Tuple[str, "Assembly"]]: + """ + Yield (name, child) pairs in a bottom-up manner + """ for ch in self.children: for el in ch.traverse(): From e1bedca8fc1006d9aed69026f820235100fd3c74 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 10:22:31 +0200 Subject: [PATCH 31/70] fixed exportCAF file overwriting NB: app/doc API of OCCT is rather non-intuitive --- cadquery/occ_impl/exporters/assembly.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index 88a0ae9e4..d314d5640 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -1,3 +1,5 @@ +import os.path + from OCP.XSControl import XSControl_WorkSession from OCP.STEPCAFControl import STEPCAFControl_Writer from OCP.STEPControl import STEPControl_StepModelType @@ -19,9 +21,9 @@ def exportAssembly(assy: AssemblyProtocol, path: str) -> bool: session = XSControl_WorkSession() writer = STEPCAFControl_Writer(session, False) - writer.SetNameMode(True) writer.SetColorMode(True) writer.SetLayerMode(True) + writer.SetNameMode(True) writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs) status = writer.Write(path) @@ -31,6 +33,10 @@ def exportAssembly(assy: AssemblyProtocol, path: str) -> bool: def exportCAF(assy: AssemblyProtocol, path: str) -> bool: + folder, fname = os.path.split(path) + name, ext = os.path.splitext(fname) + ext = ext[1:] if ext[0] == "." else ext + _, doc = toCAF(assy) app = XCAFApp_Application.GetApplication_s() @@ -42,11 +48,14 @@ def exportCAF(assy: AssemblyProtocol, path: str) -> bool: app.DefineFormat( TCollection_AsciiString("XmlOcaf"), TCollection_AsciiString("Xml XCAF Document"), - TCollection_AsciiString(path.split(".")[-1]), + TCollection_AsciiString(ext), ret, store, ) + doc.SetRequestedFolder(TCollection_ExtendedString(folder)) + doc.SetRequestedName(TCollection_ExtendedString(name)) + status = app.SaveAs(doc, TCollection_ExtendedString(path)) app.Close(doc) From 7e645650de0a81893c94669ff66492b2c614062e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 11:54:22 +0200 Subject: [PATCH 32/70] Dummy docstring to trigger sphinx inclusion --- cadquery/assembly.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index dbec1a611..7446e9f95 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -111,6 +111,9 @@ def add( ... def add(self, arg, **kwargs): + """ + add a subassembly to the current assembly. + """ if isinstance(arg, Assembly): From e3caf7b9315e6f8d2990398f709320335258a06b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 11:54:52 +0200 Subject: [PATCH 33/70] Make cq_directive sphinx 3 compatibile --- cadquery/cq_directive.py | 105 +++++++++++++++++++-------------------- environment.yml | 2 +- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/cadquery/cq_directive.py b/cadquery/cq_directive.py index bfbfc0de0..1722c7d86 100644 --- a/cadquery/cq_directive.py +++ b/cadquery/cq_directive.py @@ -4,10 +4,9 @@ """ import traceback -from cadquery import * +from cadquery import exporters from cadquery import cqgi -import io -from docutils.parsers.rst import directives +from docutils.parsers.rst import directives, Directive template = """ @@ -23,60 +22,66 @@ template_content_indent = " " -def cq_directive( - name, - arguments, - options, - content, - lineno, - content_offset, - block_text, - state, - state_machine, -): - # only consider inline snippets - plot_code = "\n".join(content) +class cq_directive(Directive): - # Since we don't have a filename, use a hash based on the content - # the script must define a variable called 'out', which is expected to - # be a CQ object - out_svg = "Your Script Did not assign call build_output() function!" + has_content = True + required_arguments = 0 + optional_arguments = 2 + option_spec = { + "height": directives.length_or_unitless, + "width": directives.length_or_percentage_or_unitless, + "align": directives.unchanged, + } + + def run(self): + + options = self.options + content = self.content + state_machine = self.state_machine + + # only consider inline snippets + plot_code = "\n".join(content) - try: - _s = io.StringIO() - result = cqgi.parse(plot_code).build() + # Since we don't have a filename, use a hash based on the content + # the script must define a variable called 'out', which is expected to + # be a CQ object + out_svg = "Your Script Did not assign call build_output() function!" - if result.success: - exporters.exportShape(result.first_result.shape, "SVG", _s) - out_svg = _s.getvalue() - else: - raise result.exception + try: + result = cqgi.parse(plot_code).build() - except Exception: - traceback.print_exc() - out_svg = traceback.format_exc() + if result.success: + out_svg = exporters.getSVG( + exporters.toCompound(result.first_result.shape) + ) + else: + raise result.exception - # now out - # Now start generating the lines of output - lines = [] + except Exception: + traceback.print_exc() + out_svg = traceback.format_exc() - # get rid of new lines - out_svg = out_svg.replace("\n", "") + # now out + # Now start generating the lines of output + lines = [] - txt_align = "left" - if "align" in options: - txt_align = options["align"] + # get rid of new lines + out_svg = out_svg.replace("\n", "") - lines.extend((template % locals()).split("\n")) + txt_align = "left" + if "align" in options: + txt_align = options["align"] - lines.extend(["::", ""]) - lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")]) - lines.append("") + lines.extend((template % locals()).split("\n")) - if len(lines): - state_machine.insert_input(lines, state_machine.input_lines.source(0)) + lines.extend(["::", ""]) + lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")]) + lines.append("") - return [] + if len(lines): + state_machine.insert_input(lines, state_machine.input_lines.source(0)) + + return [] def setup(app): @@ -84,10 +89,4 @@ def setup(app): setup.config = app.config setup.confdir = app.confdir - options = { - "height": directives.length_or_unitless, - "width": directives.length_or_percentage_or_unitless, - "align": directives.unchanged, - } - - app.add_directive("cq_plot", cq_directive, True, (0, 2, 0), **options) + app.add_directive("cq_plot", cq_directive) diff --git a/environment.yml b/environment.yml index 3a47d745f..48589bd3c 100644 --- a/environment.yml +++ b/environment.yml @@ -8,7 +8,7 @@ dependencies: - ipython - ocp - pyparsing - - sphinx=2.4 + - sphinx=3.2.1 - sphinx_rtd_theme - black - codecov From 3dc54d69e24d6242ea833e45771fcbae49cd56ac Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 12:25:30 +0200 Subject: [PATCH 34/70] Use sphinx_autodoc_typehints --- doc/conf.py | 3 +++ environment.yml | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index 4e8517aa6..a88d97e63 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -35,11 +35,14 @@ # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", "sphinx.ext.viewcode", "sphinx.ext.autosummary", "cadquery.cq_directive", ] +always_document_param_types = True + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/environment.yml b/environment.yml index 48589bd3c..4f23c89ed 100644 --- a/environment.yml +++ b/environment.yml @@ -10,6 +10,7 @@ dependencies: - pyparsing - sphinx=3.2.1 - sphinx_rtd_theme + - sphinx-autodoc-typehints - black - codecov - pytest From e437f97e3637771d7f8e7663dfcb3e795ada7519 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 14:33:45 +0200 Subject: [PATCH 35/70] Initial implementation of constraint adding --- cadquery/assembly.py | 56 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 7446e9f95..43cef5e44 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -16,9 +16,18 @@ class Constraint(object): - objects: Tuple[Shape, ...] + objects: Tuple[str, ...] args: Tuple[Shape, ...] kind: ConstraintKinds + + def __init__(self, objects: Tuple[str, ...], args: Tuple[Shape, ...], kind: ConstraintKinds): + """ + Construct a constraint. + """ + + self.objects = objects + self.args = args + self.kind = kind class Assembly(object): @@ -142,9 +151,30 @@ def add(self, arg, **kwargs): return self - @overload - def constrain(self, query: str) -> "Assembly": - ... + + def _query(self, q: str) -> Tuple[str, Optional[Shape]]: + """ + Execute a selector query on the assembly. + The query is expected to be in the following format: + + name@kind@args + + for example: + + obj_name@faces@>Z + + """ + + name, kind, arg = q.split('@') + + tmp = Workplane() + obj = self.objects[name] + + if isinstance(obj, (Workplane, Shape)): + tmp.add(obj) + res = getattr(tmp, kind)(arg) + + return name, res.val() if isinstance(res.val(), Shape) else None @overload def constrain(self, q1: str, q2: str, kind: ConstraintKinds) -> "Assembly": @@ -157,8 +187,22 @@ def constrain( ... def constrain(self, *args): - - raise NotImplementedError + """ + Define a new constraint. + """ + + if len(args) == 3: + q1, q2, kind = args + id1, s1 = self._query(q1) + id2, s2 = self._query(q2) + elif len(args) == 5: + id1, s1, id2, s2, kind = args + else: + raise ValueError(f'Incompatibile arguments: {args}') + + self.constraints.append(Constraint((id1, id2), (s1, s2), kind)) + + return self def solve(self) -> "Assembly": From d185e0374a0d7c15c3cde873889962b86804f1cf Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 14:34:04 +0200 Subject: [PATCH 36/70] Minor docstring fix --- cadquery/occ_impl/shapes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 6adf6aeee..9ce1680a5 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -314,8 +314,7 @@ def fix(obj: TopoDS_Shape) -> TopoDS_Shape: class Shape(object): """ - Represents a shape in the system. - Wrappers the FreeCAD apiSh + Represents a shape in the system.Wraps TopoDS_Shape. """ wrapped: TopoDS_Shape From cbe5a74332bc5850d194f77a9b417dbe1e5e08db Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 15:36:28 +0200 Subject: [PATCH 37/70] Import Constraint in the main namespace --- cadquery/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cadquery/__init__.py b/cadquery/__init__.py index 691f0326b..d8fc51c4d 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -28,7 +28,7 @@ Selector, ) from .cq import CQ, Workplane -from .assembly import Assembly, Color +from .assembly import Assembly, Color, Constraint from . import selectors from . import plugins @@ -38,6 +38,7 @@ "Workplane", "Assembly", "Color", + "Constraint", "plugins", "selectors", "Plane", From 39c7640a41d69f29faff9f690f89643a2a3896e4 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 15:36:55 +0200 Subject: [PATCH 38/70] Added optional constraint param --- cadquery/assembly.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 43cef5e44..4a2d6e89c 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -15,12 +15,16 @@ class Constraint(object): + """ + Geometrical constraint between two shapes of an assembly. + """ objects: Tuple[str, ...] args: Tuple[Shape, ...] kind: ConstraintKinds + param: Any - def __init__(self, objects: Tuple[str, ...], args: Tuple[Shape, ...], kind: ConstraintKinds): + def __init__(self, objects: Tuple[str, ...], args: Tuple[Shape, ...], kind: ConstraintKinds, param: Any = None): """ Construct a constraint. """ @@ -28,6 +32,7 @@ def __init__(self, objects: Tuple[str, ...], args: Tuple[Shape, ...], kind: Cons self.objects = objects self.args = args self.kind = kind + self.param = param class Assembly(object): @@ -81,6 +86,7 @@ def __init__( self.parent = None self.children = [] + self.constraints = [] self.objects = {self.name: self.obj} @overload @@ -177,12 +183,12 @@ def _query(self, q: str) -> Tuple[str, Optional[Shape]]: return name, res.val() if isinstance(res.val(), Shape) else None @overload - def constrain(self, q1: str, q2: str, kind: ConstraintKinds) -> "Assembly": + def constrain(self, q1: str, q2: str, kind: ConstraintKinds, param: Any=None) -> "Assembly": ... @overload def constrain( - self, id1: str, s1: Shape, id2: str, s2: Shape, kind: ConstraintKinds + self, id1: str, s1: Shape, id2: str, s2: Shape, kind: ConstraintKinds, param: Any=None ) -> "Assembly": ... @@ -191,16 +197,16 @@ def constrain(self, *args): Define a new constraint. """ - if len(args) == 3: - q1, q2, kind = args + if len(args) == 4: + q1, q2, kind, param = args id1, s1 = self._query(q1) id2, s2 = self._query(q2) - elif len(args) == 5: - id1, s1, id2, s2, kind = args + elif len(args) == 6: + id1, s1, id2, s2, kind, param = args else: raise ValueError(f'Incompatibile arguments: {args}') - self.constraints.append(Constraint((id1, id2), (s1, s2), kind)) + self.constraints.append(Constraint((id1, id2), (s1, s2), kind, param)) return self From ef9f5af164bce6d6d182ad5d5e2d7ca648ffa03f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 7 Sep 2020 15:37:13 +0200 Subject: [PATCH 39/70] Doc tweaks --- cadquery/occ_impl/assembly.py | 3 +++ doc/apireference.rst | 3 +++ doc/classreference.rst | 1 + 3 files changed, 7 insertions(+) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index fe0b38fb0..3ba2efa25 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -14,6 +14,9 @@ class Color(object): + """ + Wrapper for the OCCT color object Quantity_ColorRGBA. + """ wrapped: Quantity_ColorRGBA diff --git a/doc/apireference.rst b/doc/apireference.rst index d2a3035cc..cd1ea622b 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -198,3 +198,6 @@ Workplane and Shape objects can be connected together into assemblies Assembly Assembly.add Assembly.save + Assembly.constrain + Constraint + Color diff --git a/doc/classreference.rst b/doc/classreference.rst index 58261c45d..83579446a 100644 --- a/doc/classreference.rst +++ b/doc/classreference.rst @@ -19,6 +19,7 @@ Core Classes CQ Workplane Assembly + Constraint Topological Classes ---------------------- From 3e57e3a32a57bb7712998d83fde27cacbaa1b424 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 8 Sep 2020 21:57:52 +0200 Subject: [PATCH 40/70] Constraint refactoring Store sublocation in the case of of a nested object being referred to --- cadquery/assembly.py | 119 ++++++++++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 31 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 4a2d6e89c..4e8662402 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -1,4 +1,5 @@ -from typing import Union, Optional, List, Mapping, Any, overload, Tuple, Iterator, cast +from functools import reduce +from typing import Union, Optional, List, Dict, Any, overload, Tuple, Iterator, cast from typing_extensions import Literal from uuid import uuid1 as uuid @@ -21,16 +22,31 @@ class Constraint(object): objects: Tuple[str, ...] args: Tuple[Shape, ...] + locs: Tuple[Location, ...] kind: ConstraintKinds param: Any - - def __init__(self, objects: Tuple[str, ...], args: Tuple[Shape, ...], kind: ConstraintKinds, param: Any = None): + + def __init__( + self, + objects: Tuple[str, ...], + args: Tuple[Shape, ...], + locs: Tuple[Location, ...], + kind: ConstraintKinds, + param: Any = None, + ): """ Construct a constraint. - """ + :param objects: object names refernced in the constraint + :param args: subshapes (e.g. faces or edges) of the objects + :param locs: locations of the objects (only relevant if the objects are nested in a sub-assembly) + :param kind: constraint kind + :param param: optional arbitrary paramter passed to the solver + """ + self.objects = objects self.args = args + self.locs = locs self.kind = kind self.param = param @@ -42,13 +58,13 @@ class Assembly(object): loc: Location name: str color: Optional[Color] - metadata: Mapping[str, Any] + metadata: Dict[str, Any] obj: AssemblyObjects parent: Optional["Assembly"] children: List["Assembly"] - objects: Mapping[str, AssemblyObjects] + objects: Dict[str, "Assembly"] constraints: List[Constraint] def __init__( @@ -87,7 +103,24 @@ def __init__( self.children = [] self.constraints = [] - self.objects = {self.name: self.obj} + self.objects = {self.name: self} + + def _copy(self) -> "Assembly": + """ + Make a deep copy of an assembly + """ + + rv = self.__class__(self.obj, self.loc, self.name, self.color) + + for ch in self.children: + ch_copy = ch._copy() + ch_copy.parent = rv + + rv.children.append(ch_copy) + rv.objects[ch_copy.name] = ch_copy + rv.objects.update(ch_copy.objects) + + return rv @overload def add( @@ -132,19 +165,14 @@ 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, - kwargs["color"] if kwargs.get("color") else arg.color, - ) + subassy = arg._copy() - subassy.children.extend(arg.children) - subassy.objects[subassy.name] = subassy.obj - subassy.objects.update(arg.objects) + subassy.loc = kwargs["loc"] if kwargs.get("loc") else arg.loc + subassy.name = kwargs["name"] if kwargs.get("name") else arg.name + subassy.color = kwargs["color"] if kwargs.get("color") else arg.color self.children.append(subassy) - self.objects[subassy.name] = subassy.obj + self.objects[subassy.name] = subassy self.objects.update(subassy.objects) arg.parent = self @@ -157,7 +185,6 @@ def add(self, arg, **kwargs): return self - def _query(self, q: str) -> Tuple[str, Optional[Shape]]: """ Execute a selector query on the assembly. @@ -170,25 +197,51 @@ def _query(self, q: str) -> Tuple[str, Optional[Shape]]: obj_name@faces@>Z """ - - name, kind, arg = q.split('@') - + + name, kind, arg = q.split("@") + tmp = Workplane() - obj = self.objects[name] - + obj = self.objects[name].obj + if isinstance(obj, (Workplane, Shape)): tmp.add(obj) res = getattr(tmp, kind)(arg) - + return name, res.val() if isinstance(res.val(), Shape) else None + def _subloc(self, name: str) -> Location: + """ + Calculate relative location of an object in a subassembly. + """ + + rv = Location() + obj = self.objects[name] + + if obj not in self.children: + locs = [] + while not obj.parent is self: + locs.append(obj.loc) + obj = cast(Assembly, obj.parent) + + rv = reduce(lambda l1, l2: l1 * l2, locs) + + return rv + @overload - def constrain(self, q1: str, q2: str, kind: ConstraintKinds, param: Any=None) -> "Assembly": + def constrain( + self, q1: str, q2: str, kind: ConstraintKinds, param: Any = None + ) -> "Assembly": ... @overload def constrain( - self, id1: str, s1: Shape, id2: str, s2: Shape, kind: ConstraintKinds, param: Any=None + self, + id1: str, + s1: Shape, + id2: str, + s2: Shape, + kind: ConstraintKinds, + param: Any = None, ) -> "Assembly": ... @@ -196,7 +249,7 @@ def constrain(self, *args): """ Define a new constraint. """ - + if len(args) == 4: q1, q2, kind, param = args id1, s1 = self._query(q1) @@ -204,10 +257,14 @@ def constrain(self, *args): elif len(args) == 6: id1, s1, id2, s2, kind, param = args else: - raise ValueError(f'Incompatibile arguments: {args}') - - self.constraints.append(Constraint((id1, id2), (s1, s2), kind, param)) - + raise ValueError(f"Incompatibile arguments: {args}") + + loc1 = self._subloc(id1) + loc2 = self._subloc(id2) + self.constraints.append( + Constraint((id1, id2), (s1, s2), (loc1, loc2), kind, param) + ) + return self def solve(self) -> "Assembly": From 8fc43ca536513bd69e3edaca88678eaebe5bb492 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 10:59:15 +0200 Subject: [PATCH 41/70] Pin black version for now --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e622ef93c..96c6d1143 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,6 +41,7 @@ matrix: env: PYTHON_VERSION=3.7 os: linux script: + - pip install black==19.10b0 - black . --diff --check - mypy cadquery From f53bd441aaff362e3c5f25513208e16519661a94 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 11:36:12 +0200 Subject: [PATCH 42/70] Working colors in STEP export --- cadquery/occ_impl/assembly.py | 17 ++++++++++++++--- cadquery/occ_impl/exporters/assembly.py | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 3ba2efa25..2e40f90b0 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -95,7 +95,7 @@ def setColor(l: TDF_Label, color: Color, tool): tool.SetColor(l, color.wrapped, XCAFDoc_ColorType.XCAFDoc_ColorSurf) -def toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]: +def toCAF(assy: AssemblyProtocol, coloredSTEP: bool = False) -> Tuple[TDF_Label, TDocStd_Document]: # prepare a doc doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf")) @@ -119,8 +119,19 @@ def toCAF(assy: AssemblyProtocol) -> Tuple[TDF_Label, TDocStd_Document]: subassy = tool.NewShape() tool.AddComponent(subassy, lab, TopLoc_Location()) setName(subassy, k, tool) - if v.color: - setColor(subassy, v.color, ctool) + + # handle colors - this logic is needed for proper STEP export + color = v.color + tmp = v + if coloredSTEP: + while not color and tmp.parent: + tmp = tmp.parent + color = tmp.color + if color: + setColor(lab, color, ctool) + else: + if color: + setColor(subassy, color, ctool) subassys[k] = (subassy, v.loc) diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index d314d5640..146ea6558 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -17,7 +17,7 @@ def exportAssembly(assy: AssemblyProtocol, path: str) -> bool: - _, doc = toCAF(assy) + _, doc = toCAF(assy, True) session = XSControl_WorkSession() writer = STEPCAFControl_Writer(session, False) From c8c21d7d1175b908ab8637835b49f0f1d880cff7 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 11:48:08 +0200 Subject: [PATCH 43/70] Black fix --- cadquery/occ_impl/assembly.py | 6 ++++-- tests/test_cad_objects.py | 11 +++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 2e40f90b0..7884be191 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -95,7 +95,9 @@ def setColor(l: TDF_Label, color: Color, tool): tool.SetColor(l, color.wrapped, XCAFDoc_ColorType.XCAFDoc_ColorSurf) -def toCAF(assy: AssemblyProtocol, coloredSTEP: bool = False) -> Tuple[TDF_Label, TDocStd_Document]: +def toCAF( + assy: AssemblyProtocol, coloredSTEP: bool = False +) -> Tuple[TDF_Label, TDocStd_Document]: # prepare a doc doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf")) @@ -119,7 +121,7 @@ def toCAF(assy: AssemblyProtocol, coloredSTEP: bool = False) -> Tuple[TDF_Label, subassy = tool.NewShape() tool.AddComponent(subassy, lab, TopLoc_Location()) setName(subassy, k, tool) - + # handle colors - this logic is needed for proper STEP export color = v.color tmp = v diff --git a/tests/test_cad_objects.py b/tests/test_cad_objects.py index bfa2e22c2..dbba7b172 100644 --- a/tests/test_cad_objects.py +++ b/tests/test_cad_objects.py @@ -395,7 +395,7 @@ def testPlaneNotEqual(self): ) def testLocation(self): - + # Vector loc1 = Location(Vector(0, 0, 1)) @@ -410,10 +410,13 @@ def testLocation(self): # gp_Trsf T = gp_Trsf() - T.SetTranslation(gp_Vec(0,0,1)) + T.SetTranslation(gp_Vec(0, 0, 1)) loc3 = Location(T) - - assert( loc1.wrapped.Transformation().TranslationPart().Z() == loc3.wrapped.Transformation().TranslationPart().Z()) + + assert ( + loc1.wrapped.Transformation().TranslationPart().Z() + == loc3.wrapped.Transformation().TranslationPart().Z() + ) if __name__ == "__main__": From 1963a65972939673d2677a3a719bb704e15994b3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 14:24:05 +0200 Subject: [PATCH 44/70] Implemented init --- cadquery/occ_impl/solver.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index b99d08d9a..569b422a2 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -15,7 +15,7 @@ class ConstraintSolver(object): - entities: Mapping[int, DOF6] + entities: List[DOF6] constraints: Mapping[Tuple[int, int], Constraint] ne: int nc: int @@ -26,7 +26,24 @@ def __init__( constraints: Mapping[Tuple[int, int], Constraint], ): - pass + self.entities = [self._locToDOF6(loc) for loc in entities] + self.constraints = constraints + self.ne = len(entities) + self.nc = len(constraints) + + @staticmethod + def _locToDOF6(loc: Location) -> DOF6: + + T = loc.wrapped.Transformation() + v = T.TranslationPart() + q = T.GetRotation() + + alpha_2 = (1 - q.W()) / (1 + q.W()) + a = (alpha_2 + 1) * q.X() / 2 + b = (alpha_2 + 1) * q.Y() / 2 + c = (alpha_2 + 1) * q.Z() / 2 + + return (v.X(), v.Y(), v.Z(), a, b, c) def _jacobianSparsity(self) -> Array[(Any, Any), float]: @@ -82,7 +99,7 @@ def f(x): def solve(self) -> List[Location]: - x0 = array([el for el in self.entities.values()]).ravel() + x0 = array([el for el in self.entities]).ravel() res = least_squares(self._cost(), x0, jac_sparsity=self._jacobianSparsity()) x = res.x From c15c364da626da172facb3765a74e7f1a3c5cf21 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 14:26:09 +0200 Subject: [PATCH 45/70] Implemented constraint translation to the POD format --- cadquery/assembly.py | 44 +++++++++++++++++++++++++++++++++++-- cadquery/occ_impl/shapes.py | 22 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 4e8662402..160632011 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -4,9 +4,14 @@ 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.shapes import Shape, Face, Edge, Wire +from .occ_impl.geom import Location, Vector from .occ_impl.assembly import Color +from .occ_impl.solver import ( + ConstraintSolver, + ConstraintMarker, + Constraint as ConstraintPOD, +) from .occ_impl.exporters.assembly import exportAssembly, exportCAF @@ -50,6 +55,41 @@ def __init__( self.kind = kind self.param = param + def _getAxis(self, arg: Shape) -> Vector: + + if isinstance(arg, Face): + rv = arg.normalAt() + elif isinstance(arg, Edge) and arg.geomType() != "CIRCLE": + rv = arg.tangentAt() + elif isinstance(arg, Edge) and arg.geomType() == "CIRCLE": + rv = arg.normal() + else: + raise ValueError(f"Cannot construct Axis for {arg}") + + return rv + + def toPOD(self) -> ConstraintPOD: + """ + Convert the constraint to a representation used by the solver. + """ + + rv: List[Tuple[ConstraintMarker, ...]] = [] + + for arg, loc in zip(self.args, self.locs): + + arg = arg.moved(loc) + + if self.kind == "Axis": + rv.append((self._getAxis(arg).toDir(),)) + elif self.kind == "Point": + rv.append((arg.Center().toPnt(),)) + elif self.kind == "Plane": + rv.append((self._getAxis(arg).toDir(), arg.Center().toPnt())) + else: + raise ValueError(f"Unknown constraint kind {self.kind}") + + return cast(ConstraintPOD, tuple(rv)) + class Assembly(object): """Nested assembly of Workplane and Shape objects defining their relative positions. diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 9ce1680a5..4b4c88aa1 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -977,6 +977,28 @@ def tangentAt(self, locationParam: float = 0.5) -> Vector: return rv + def normal(self) -> Vector: + """ + Calculate normal Vector. Only possible for CIRCLE or ELLIPSE + + :param locationParam: location to use in [0,1] + :return: tangent vector + """ + + curve = self._geomAdaptor() + gtype = self.geomType() + + if gtype == "CIRCLE": + circ = curve.Circle() + rv = Vector(circ.Axis().Direction()) + elif gtype == "ELLIPSE": + ell = curve.Ellipse() + rv = Vector(ell.Axis().Direction()) + else: + raise ValueError(f"{gtype} has no normal") + + return rv + def Center(self) -> Vector: Properties = GProp_GProps() From 46dd8206ae627d64ff4c52609c8771ccfc40c070 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 14:26:46 +0200 Subject: [PATCH 46/70] Added mising property --- cadquery/occ_impl/assembly.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 7884be191..adb832cda 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -69,6 +69,10 @@ def loc(self) -> Location: def name(self) -> str: ... + @property + def parent(self) -> Optional["AssemblyProtocol"]: + ... + @property def color(self) -> Optional[Color]: ... From 146bbe577002805f070737e4eda8c848cc6fa0c2 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 15:29:39 +0200 Subject: [PATCH 47/70] Add nptyping to req --- conda/meta.yaml | 1 + environment.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/conda/meta.yaml b/conda/meta.yaml index 4d112d681..6b1eb217b 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -20,6 +20,7 @@ requirements: - ezdxf - ipython - typing_extensions + - nptyping test: requires: diff --git a/environment.yml b/environment.yml index 1125ad75d..b7a078632 100644 --- a/environment.yml +++ b/environment.yml @@ -18,6 +18,7 @@ dependencies: - ezdxf - ipython - typing_extensions + - nptyping - pip - pip: - "--editable=." From e8cba7d05cd367087b808a2dcfdce383850a881a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 9 Sep 2020 15:45:33 +0200 Subject: [PATCH 48/70] Add scipy to the specs --- conda/meta.yaml | 1 + environment.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/conda/meta.yaml b/conda/meta.yaml index 6b1eb217b..4e55c5534 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -21,6 +21,7 @@ requirements: - ipython - typing_extensions - nptyping + - scipy test: requires: diff --git a/environment.yml b/environment.yml index b7a078632..6c9ceb132 100644 --- a/environment.yml +++ b/environment.yml @@ -19,6 +19,7 @@ dependencies: - ipython - typing_extensions - nptyping + - scipy - pip - pip: - "--editable=." From dd863bdbc58dbb29a53b0bb5fe0854591232d07f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 11 Sep 2020 20:25:48 +0200 Subject: [PATCH 49/70] Added test for normal --- tests/test_cadquery.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 295a764f3..94b2f6415 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3606,3 +3606,19 @@ def testLocationAt(self): self.assertAlmostEqual(T3.TranslationPart().X(), r, 6) self.assertAlmostEqual(T4.TranslationPart().X(), r, 6) + + def testNormal(self): + + circ = Workplane().circle(1).edges().val() + n = circ.normal() + + self.assertTupleAlmostEquals(n.toTuple(), (0, 0, 1), 6) + + ell = Workplane().ellipse(1, 2).edges().val() + n = circ.normal() + + self.assertTupleAlmostEquals(n.toTuple(), (0, 0, 1), 6) + + with self.assertRaises(ValueError): + edge = Workplane().rect(1, 2).edges().val() + n = edge.normal() From f914815019ff5f9dd5769097fe22270c15475ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Fri, 11 Sep 2020 23:54:23 +0200 Subject: [PATCH 50/70] Fixed normal test --- tests/test_cadquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 94b2f6415..3cbda4d21 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3615,7 +3615,7 @@ def testNormal(self): self.assertTupleAlmostEquals(n.toTuple(), (0, 0, 1), 6) ell = Workplane().ellipse(1, 2).edges().val() - n = circ.normal() + n = ell.normal() self.assertTupleAlmostEquals(n.toTuple(), (0, 0, 1), 6) From c5676b619a403af555356c006f1b64ee53fa93f5 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 13 Sep 2020 17:21:40 +0200 Subject: [PATCH 51/70] First pass on solve() --- cadquery/assembly.py | 48 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 160632011..7ce1f01fb 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -4,7 +4,7 @@ from uuid import uuid1 as uuid from .cq import Workplane -from .occ_impl.shapes import Shape, Face, Edge, Wire +from .occ_impl.shapes import Shape, Face, Edge from .occ_impl.geom import Location, Vector from .occ_impl.assembly import Color from .occ_impl.solver import ( @@ -249,23 +249,27 @@ def _query(self, q: str) -> Tuple[str, Optional[Shape]]: return name, res.val() if isinstance(res.val(), Shape) else None - def _subloc(self, name: str) -> Location: + def _subloc(self, name: str) -> Tuple[Location, str]: """ Calculate relative location of an object in a subassembly. + + Returns the relative posiitons as well as the name of the top assembly. """ rv = Location() obj = self.objects[name] + name_out = name if obj not in self.children: locs = [] while not obj.parent is self: locs.append(obj.loc) obj = cast(Assembly, obj.parent) + name_out = obj.name rv = reduce(lambda l1, l2: l1 * l2, locs) - return rv + return (rv, name_out) @overload def constrain( @@ -299,17 +303,47 @@ def constrain(self, *args): else: raise ValueError(f"Incompatibile arguments: {args}") - loc1 = self._subloc(id1) - loc2 = self._subloc(id2) + loc1, id1_top = self._subloc(id1) + loc2, id2_top = self._subloc(id2) self.constraints.append( - Constraint((id1, id2), (s1, s2), (loc1, loc2), kind, param) + Constraint((id1_top, id2_top), (s1, s2), (loc1, loc2), kind, param) ) return self def solve(self) -> "Assembly": + """ + Solve the constraints. + """ - raise NotImplementedError + # get all entities and number them + ents = {} + + i = 0 + for c in self.constraints: + for name in c.objects: + if name not in ents: + ents[name] = i + i += 1 + + locs = [self.objects[n].loc for n in ents] + + # construct the constraint mapping + c_mapping = {} + for c in self.constraints: + c_mapping[(ents[c.objects[0]], ents[c.objects[1]])] = c.toPOD() + + # instantiate the solver + solver = ConstraintSolver(locs, c_mapping) + + # solve + locs_new = solver.solve() + + # update positions + for loc_new, n in zip(locs_new, ents): + self.objects[n].loc = loc_new + + return self def save( self, path: str, exportType: Optional[ExportLiterals] = None From c3432b6131ebdf3b3ca08a62e5328cbd34091e19 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 13 Sep 2020 20:38:01 +0200 Subject: [PATCH 52/70] Added tests for constrain and solve --- tests/test_assembly.py | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index d412d7409..ce949e06b 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -4,6 +4,8 @@ import cadquery as cq from cadquery.occ_impl.exporters.assembly import exportAssembly, exportCAF +from OCP.gp import gp_XYZ + @pytest.fixture def simple_assy(): @@ -65,6 +67,7 @@ def test_assembly(simple_assy, nested_assy): assert len(nested_assy.objects) == 3 assert len(nested_assy.children) == 1 + assert nested_assy.objects["SECOND"].parent is nested_assy # bottm-up traversal kvs = list(nested_assy.traverse()) @@ -95,7 +98,7 @@ def test_native_export(simple_assy): assert os.path.exists("assy.xml") -def test_save(simple_assy): +def test_save(simple_assy, nested_assy): simple_assy.save("simple.step") assert os.path.exists("simple.step") @@ -117,3 +120,49 @@ def test_save(simple_assy): with pytest.raises(ValueError): simple_assy.save("simple.step", "DXF") + + +def test_constrain(simple_assy, nested_assy): + + subassy1 = simple_assy.children[0] + subassy2 = simple_assy.children[1] + + b1 = simple_assy.obj + b2 = subassy1.obj + b3 = subassy2.obj + + simple_assy.constrain( + simple_assy.name, b1.Faces()[0], subassy1.name, b2.faces("Z").val(), + subassy2.name, + b3.faces("Z", "BOTTOM@faces@ Date: Sun, 13 Sep 2020 20:38:19 +0200 Subject: [PATCH 53/70] Fixed failures for constrain --- cadquery/assembly.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 7ce1f01fb..4eedc9e64 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -27,7 +27,7 @@ class Constraint(object): objects: Tuple[str, ...] args: Tuple[Shape, ...] - locs: Tuple[Location, ...] + sublocs: Tuple[Location, ...] kind: ConstraintKinds param: Any @@ -35,7 +35,7 @@ def __init__( self, objects: Tuple[str, ...], args: Tuple[Shape, ...], - locs: Tuple[Location, ...], + sublocs: Tuple[Location, ...], kind: ConstraintKinds, param: Any = None, ): @@ -44,14 +44,14 @@ def __init__( :param objects: object names refernced in the constraint :param args: subshapes (e.g. faces or edges) of the objects - :param locs: locations of the objects (only relevant if the objects are nested in a sub-assembly) + :param sublocs: locations of the objects (only relevant if the objects are nested in a sub-assembly) :param kind: constraint kind :param param: optional arbitrary paramter passed to the solver """ self.objects = objects self.args = args - self.locs = locs + self.sublocs = sublocs self.kind = kind self.param = param @@ -75,7 +75,7 @@ def toPOD(self) -> ConstraintPOD: rv: List[Tuple[ConstraintMarker, ...]] = [] - for arg, loc in zip(self.args, self.locs): + for arg, loc in zip(self.args, self.sublocs): arg = arg.moved(loc) @@ -210,6 +210,7 @@ def add(self, arg, **kwargs): subassy.loc = kwargs["loc"] if kwargs.get("loc") else arg.loc subassy.name = kwargs["name"] if kwargs.get("name") else arg.name subassy.color = kwargs["color"] if kwargs.get("color") else arg.color + subassy.parent = self self.children.append(subassy) self.objects[subassy.name] = subassy @@ -260,7 +261,7 @@ def _subloc(self, name: str) -> Tuple[Location, str]: obj = self.objects[name] name_out = name - if obj not in self.children: + if obj not in self.children and obj is not self: locs = [] while not obj.parent is self: locs.append(obj.loc) @@ -289,17 +290,17 @@ def constrain( ) -> "Assembly": ... - def constrain(self, *args): + def constrain(self, *args, param=None): """ Define a new constraint. """ - if len(args) == 4: - q1, q2, kind, param = args + if len(args) == 3: + q1, q2, kind = args id1, s1 = self._query(q1) id2, s2 = self._query(q2) - elif len(args) == 6: - id1, s1, id2, s2, kind, param = args + elif len(args) == 5: + id1, s1, id2, s2, kind = args else: raise ValueError(f"Incompatibile arguments: {args}") From 9c29c32c8ee3aa408bcc9cca65bab153b130f9aa Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 13 Sep 2020 20:38:26 +0200 Subject: [PATCH 54/70] Fixed solve issues --- cadquery/occ_impl/solver.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 569b422a2..8ae457b16 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -64,7 +64,12 @@ def _build_transform( rv.SetTranslation(gp_Vec(x, y, z)) rv.SetRotation( - gp_Quaternion(2 * a / m, 2 * b / m, 2 * c / m, (1 - m) / (m + 1)) + gp_Quaternion( + 2 * a / m if m != 0 else 0, + 2 * b / m if m != 0 else 0, + 2 * c / m if m != 0 else 0, + (1 - m) / (m + 1), + ) ) return rv @@ -81,13 +86,15 @@ def f(x): self._build_transform(*x[6 * i : 6 * (i + 1)]) for i in range(ne) ] - for i, ((k1, k2), ms1, ms2) in enumerate(constraints.items()): + for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints.items()): t1 = transforms[k1] t2 = transforms[k2] for m1, m2 in zip(ms1, ms2): if isinstance(m1, gp_Pnt): - rv[i] += (m1.Transformed(t1) - m2.Transformed(t2)).Magnitude() + rv[i] += ( + m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ() + ).Modulus() elif isinstance(m1, gp_Dir): rv[i] += m1.Transformed(t1) * m2.Transformed(t2) else: From 2cad05215b653ebcca433a637a51020bb8277a73 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 15 Sep 2020 17:57:15 +0200 Subject: [PATCH 55/70] Reworked the solver --- cadquery/assembly.py | 9 ++-- cadquery/occ_impl/solver.py | 94 +++++++++++++++++++++++++++---------- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 4eedc9e64..5cbb7ed3a 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -321,21 +321,24 @@ def solve(self) -> "Assembly": ents = {} i = 0 + lock_ix = 0 for c in self.constraints: for name in c.objects: if name not in ents: ents[name] = i + if name == self.name: + lock_ix = i i += 1 locs = [self.objects[n].loc for n in ents] # construct the constraint mapping - c_mapping = {} + constraints = [] for c in self.constraints: - c_mapping[(ents[c.objects[0]], ents[c.objects[1]])] = c.toPOD() + constraints.append(((ents[c.objects[0]], ents[c.objects[1]]), c.toPOD())) # instantiate the solver - solver = ConstraintSolver(locs, c_mapping) + solver = ConstraintSolver(locs, constraints, locked=[lock_ix]) # solve locs_new = solver.solve() diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 8ae457b16..7e6520f15 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -1,7 +1,8 @@ -from typing import Tuple, Mapping, Union, Any, Callable, List +from typing import Tuple, Union, Any, Callable, List, Optional from nptyping import NDArray as Array -from numpy import zeros, array +from numpy import zeros, array, full, inf + from scipy.optimize import least_squares from OCP.gp import gp_Vec, gp_Dir, gp_Pnt, gp_Trsf, gp_Quaternion @@ -10,26 +11,42 @@ DOF6 = Tuple[float, float, float, float, float, float] ConstraintMarker = Union[gp_Dir, gp_Pnt] -Constraint = Tuple[Tuple[ConstraintMarker, ...], Tuple[ConstraintMarker, ...]] +Constraint = Tuple[Tuple[ConstraintMarker, ...], Tuple[Optional[ConstraintMarker], ...]] + +NDOF = 6 class ConstraintSolver(object): entities: List[DOF6] - constraints: Mapping[Tuple[int, int], Constraint] + constraints: List[Tuple[Tuple[int, Optional[int]], Constraint]] + locked: List[int] ne: int nc: int def __init__( self, entities: List[Location], - constraints: Mapping[Tuple[int, int], Constraint], + constraints: List[Tuple[Tuple[int, int], Constraint]], + locked: List[int] = [], ): self.entities = [self._locToDOF6(loc) for loc in entities] - self.constraints = constraints + self.constraints = [] + + # decompose inot simple constraints + for k, v in constraints: + e1, e2 = v + if e2: + for m1, m2 in zip(e1, e2): + self.constraints.append((k, ((m1,), (m2,)))) + else: + for m1 in e1: + self.constraints.append((k, ((m1,), (None,)))) + self.ne = len(entities) - self.nc = len(constraints) + self.locked = locked + self.nc = len(self.constraints) @staticmethod def _locToDOF6(loc: Location) -> DOF6: @@ -47,11 +64,17 @@ def _locToDOF6(loc: Location) -> DOF6: def _jacobianSparsity(self) -> Array[(Any, Any), float]: - rv = zeros((self.nc, 6 * self.ne)) + rv = zeros((self.nc, NDOF * self.ne)) + + for i, ((k1, k2), ((m1,), (m2,))) in enumerate(self.constraints): - for i, (k1, k2) in enumerate(self.constraints): - rv[i, 6 * k1 : 6 * (k1 + 1)] = 1 - rv[i, 6 * k2 : 6 * (k2 + 1)] = 1 + k1_active = 1 if k1 not in self.locked else 0 + k2_active = 1 if k2 not in self.locked else 0 + + rv[i, NDOF * k1 : NDOF * (k1 + 1)] = k1_active + + if k2: + rv[i, NDOF * k2 : NDOF * (k2 + 1)] = k2_active return rv @@ -62,16 +85,14 @@ def _build_transform( rv = gp_Trsf() m = a ** 2 + b ** 2 + c ** 2 - rv.SetTranslation(gp_Vec(x, y, z)) rv.SetRotation( gp_Quaternion( - 2 * a / m if m != 0 else 0, - 2 * b / m if m != 0 else 0, - 2 * c / m if m != 0 else 0, - (1 - m) / (m + 1), + 2 * a / (m + 1), 2 * b / (m + 1), 2 * c / (m + 1), (1 - m) / (m + 1), ) ) + rv.SetTranslationPart(gp_Vec(x, y, z)) + return rv def _cost(self) -> Callable[[Array[(Any,), float]], Array[(Any,), float]]: @@ -82,13 +103,14 @@ def f(x): ne = self.ne rv = zeros(nc) + transforms = [ - self._build_transform(*x[6 * i : 6 * (i + 1)]) for i in range(ne) + self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) ] - for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints.items()): - t1 = transforms[k1] - t2 = transforms[k2] + for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): + t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() + t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() for m1, m2 in zip(ms1, ms2): if isinstance(m1, gp_Pnt): @@ -96,22 +118,44 @@ def f(x): m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ() ).Modulus() elif isinstance(m1, gp_Dir): - rv[i] += m1.Transformed(t1) * m2.Transformed(t2) + rv[i] += m1.Transformed(t1).Angle(m2.Transformed(t2)) else: - raise NotImplementedError + raise NotImplementedError(f"{m1,m2}") return rv return f + def _bounds(self) -> Tuple[Array[(Any,), float], Array[(Any,), float]]: + + bmin = full((NDOF * self.ne,), -inf) + bmax = full((NDOF * self.ne,), +inf) + + for i in self.locked: + bmin[NDOF * i : (NDOF * i + NDOF)] = self.entities[i] + bmax[NDOF * i : (NDOF * i + NDOF)] = ( + bmin[NDOF * i : (NDOF * i + NDOF)] + 1e-9 + ) + + return bmin, bmax + def solve(self) -> List[Location]: x0 = array([el for el in self.entities]).ravel() - - res = least_squares(self._cost(), x0, jac_sparsity=self._jacobianSparsity()) + res = least_squares( + self._cost(), + x0, + jac="2-point", + jac_sparsity=self._jacobianSparsity(), + method="dogbox", + ftol=None, + gtol=1e-6, + xtol=None, + verbose=2, + ) x = res.x return [ - Location(self._build_transform(*x[6 * i : 6 * (i + 1)])) + Location(self._build_transform(*x[NDOF * i : NDOF * (i + 1)])) for i in range(self.ne) ] From 824a635fe54698524301e3e654ed99bb6223fb07 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Sep 2020 22:24:25 +0200 Subject: [PATCH 56/70] Different subloc handling --- cadquery/assembly.py | 2 +- cadquery/occ_impl/shapes.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 5cbb7ed3a..6e98efa83 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -77,7 +77,7 @@ def toPOD(self) -> ConstraintPOD: for arg, loc in zip(self.args, self.sublocs): - arg = arg.moved(loc) + arg = arg.located(loc * arg.location()) if self.kind == "Axis": rv.append((self._getAxis(arg).toDir(),)) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 4b4c88aa1..55b293063 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -694,6 +694,13 @@ def transformGeometry(self, tMatrix: Matrix) -> "Shape": return r + def location(self) -> Location: + """ + Return the current location + """ + + return Location(self.wrapped.Location()) + def locate(self, loc: Location) -> "Shape": """ Apply a location in absolute sense to self From 17c8f1de4fdb8b4b2cd1eb7299a021fe80ba3b9e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 16 Sep 2020 22:25:13 +0200 Subject: [PATCH 57/70] Simplified the solver and switched to BFGS --- cadquery/occ_impl/solver.py | 61 +++++++++---------------------------- 1 file changed, 14 insertions(+), 47 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 7e6520f15..e2187425c 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -1,9 +1,8 @@ from typing import Tuple, Union, Any, Callable, List, Optional from nptyping import NDArray as Array -from numpy import zeros, array, full, inf - -from scipy.optimize import least_squares +from numpy import array +from scipy.optimize import minimize from OCP.gp import gp_Vec, gp_Dir, gp_Pnt, gp_Trsf, gp_Quaternion @@ -34,7 +33,7 @@ def __init__( self.entities = [self._locToDOF6(loc) for loc in entities] self.constraints = [] - # decompose inot simple constraints + # decompose into simple constraints for k, v in constraints: e1, e2 = v if e2: @@ -62,22 +61,6 @@ def _locToDOF6(loc: Location) -> DOF6: return (v.X(), v.Y(), v.Z(), a, b, c) - def _jacobianSparsity(self) -> Array[(Any, Any), float]: - - rv = zeros((self.nc, NDOF * self.ne)) - - for i, ((k1, k2), ((m1,), (m2,))) in enumerate(self.constraints): - - k1_active = 1 if k1 not in self.locked else 0 - k2_active = 1 if k2 not in self.locked else 0 - - rv[i, NDOF * k1 : NDOF * (k1 + 1)] = k1_active - - if k2: - rv[i, NDOF * k2 : NDOF * (k2 + 1)] = k2_active - - return rv - def _build_transform( self, x: float, y: float, z: float, a: float, b: float, c: float ) -> gp_Trsf: @@ -95,14 +78,13 @@ def _build_transform( return rv - def _cost(self) -> Callable[[Array[(Any,), float]], Array[(Any,), float]]: + def _cost(self) -> Callable[[Array[(Any,), float]], float]: def f(x): constraints = self.constraints - nc = self.nc ne = self.ne - rv = zeros(nc) + rv = 0 transforms = [ self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) @@ -114,11 +96,11 @@ def f(x): for m1, m2 in zip(ms1, ms2): if isinstance(m1, gp_Pnt): - rv[i] += ( + rv += ( m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ() - ).Modulus() + ).Modulus() ** 2 elif isinstance(m1, gp_Dir): - rv[i] += m1.Transformed(t1).Angle(m2.Transformed(t2)) + rv += (-1 - m1.Transformed(t1).Dot(m2.Transformed(t2))) ** 2 else: raise NotImplementedError(f"{m1,m2}") @@ -126,34 +108,19 @@ def f(x): return f - def _bounds(self) -> Tuple[Array[(Any,), float], Array[(Any,), float]]: - - bmin = full((NDOF * self.ne,), -inf) - bmax = full((NDOF * self.ne,), +inf) - - for i in self.locked: - bmin[NDOF * i : (NDOF * i + NDOF)] = self.entities[i] - bmax[NDOF * i : (NDOF * i + NDOF)] = ( - bmin[NDOF * i : (NDOF * i + NDOF)] + 1e-9 - ) - - return bmin, bmax - def solve(self) -> List[Location]: x0 = array([el for el in self.entities]).ravel() - res = least_squares( + + res = minimize( self._cost(), x0, - jac="2-point", - jac_sparsity=self._jacobianSparsity(), - method="dogbox", - ftol=None, - gtol=1e-6, - xtol=None, - verbose=2, + method="BFGS", + options=dict(disp=True, ftol=1e-6, maxiter=500), ) + x = res.x + print(res.message) return [ Location(self._build_transform(*x[NDOF * i : NDOF * (i + 1)])) From e01b2415211f8a3076aa84a7727d7f599720854c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 21 Sep 2020 08:16:46 +0200 Subject: [PATCH 58/70] Another solver tweak --- cadquery/occ_impl/solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index e2187425c..573f8ee66 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -115,8 +115,8 @@ def solve(self) -> List[Location]: res = minimize( self._cost(), x0, - method="BFGS", - options=dict(disp=True, ftol=1e-6, maxiter=500), + method="L-BFGS-B", + options=dict(disp=True, ftol=1e-9, maxiter=1000), ) x = res.x From f4150b62d34e6c5bd9d1faca6157e112465b1eb5 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 24 Sep 2020 22:27:47 +0200 Subject: [PATCH 59/70] Major rework of the solver * numerical jacobian by hand taking into account the sparsity pattern * better code structure * scaling of the dir constraint --- cadquery/occ_impl/solver.py | 93 +++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 573f8ee66..d50004db2 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -1,7 +1,7 @@ from typing import Tuple, Union, Any, Callable, List, Optional from nptyping import NDArray as Array -from numpy import array +from numpy import array, eye, zeros from scipy.optimize import minimize from OCP.gp import gp_Vec, gp_Dir, gp_Pnt, gp_Trsf, gp_Quaternion @@ -13,6 +13,8 @@ Constraint = Tuple[Tuple[ConstraintMarker, ...], Tuple[Optional[ConstraintMarker], ...]] NDOF = 6 +DIR_SCALING = 1e3 +DIFF_EPS = 1e-8 class ConstraintSolver(object): @@ -78,7 +80,22 @@ def _build_transform( return rv - def _cost(self) -> Callable[[Array[(Any,), float]], float]: + def _cost( + self, + ) -> Tuple[ + Callable[[Array[(Any,), float]], float], + Callable[[Array[(Any,), float]], Array[(Any,), float]], + ]: + def pt_cost(m1: gp_Pnt, m2: gp_Pnt, t1: gp_Trsf, t2: gp_Trsf) -> float: + + return (m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ()).SquareModulus() + + def dir_cost( + m1: gp_Dir, m2: gp_Dir, t1: gp_Trsf, t2: gp_Trsf, val: float = -1 + ) -> float: + + return DIR_SCALING * (val - m1.Transformed(t1).Dot(m2.Transformed(t2))) ** 2 + def f(x): constraints = self.constraints @@ -96,25 +113,85 @@ def f(x): for m1, m2 in zip(ms1, ms2): if isinstance(m1, gp_Pnt): - rv += ( - m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ() - ).Modulus() ** 2 + rv += pt_cost(m1, m2, t1, t2) + elif isinstance(m1, gp_Dir): + rv += dir_cost(m1, m2, t1, t2) + else: + raise NotImplementedError(f"{m1,m2}") + + return rv + + def jac(x): + + constraints = self.constraints + ne = self.ne + + delta = DIFF_EPS * eye(NDOF) + + rv = zeros(NDOF * ne) + + transforms = [ + self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) + ] + + transforms_delta = [ + self._build_transform(*(x[NDOF * i : NDOF * (i + 1)] + delta[j, :])) + for i in range(ne) + for j in range(NDOF) + ] + + for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): + t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() + t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() + + for m1, m2 in zip(ms1, ms2): + if isinstance(m1, gp_Pnt): + tmp = pt_cost(m1, m2, t1, t2) + + for j in range(NDOF): + + t1j = transforms_delta[k1 * NDOF + j] + t2j = transforms_delta[k2 * NDOF + j] + + if k1 not in self.locked: + tmp1 = pt_cost(m1, m2, t1j, t2) + rv[k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS + + if k2 not in self.locked: + tmp2 = pt_cost(m1, m2, t1, t2j) + rv[k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS + elif isinstance(m1, gp_Dir): - rv += (-1 - m1.Transformed(t1).Dot(m2.Transformed(t2))) ** 2 + tmp = dir_cost(m1, m2, t1, t2) + + for j in range(NDOF): + + t1j = transforms_delta[k1 * NDOF + j] + t2j = transforms_delta[k2 * NDOF + j] + + if k1 not in self.locked: + tmp1 = dir_cost(m1, m2, t1j, t2) + rv[k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS + + if k2 not in self.locked: + tmp2 = dir_cost(m1, m2, t1, t2j) + rv[k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS else: raise NotImplementedError(f"{m1,m2}") return rv - return f + return f, jac def solve(self) -> List[Location]: x0 = array([el for el in self.entities]).ravel() + f, jac = self._cost() res = minimize( - self._cost(), + f, x0, + jac=jac, method="L-BFGS-B", options=dict(disp=True, ftol=1e-9, maxiter=1000), ) From e3cb80cbcd0e5c34c5dd8df3ac1017d209e45ca3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 25 Sep 2020 08:57:05 +0200 Subject: [PATCH 60/70] Tighter ftol --- cadquery/occ_impl/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index d50004db2..77e31a12c 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -193,7 +193,7 @@ def solve(self) -> List[Location]: x0, jac=jac, method="L-BFGS-B", - options=dict(disp=True, ftol=1e-9, maxiter=1000), + options=dict(disp=True, ftol=1e-14, maxiter=1000), ) x = res.x From b6484959edabfaa95c3bd39dddfd89de6b1010f3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 25 Sep 2020 08:57:24 +0200 Subject: [PATCH 61/70] Better assy.solve() test --- tests/test_assembly.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index ce949e06b..28220d97f 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -148,8 +148,9 @@ def test_constrain(simple_assy, nested_assy): assert len(simple_assy.constraints) == 3 nested_assy.constrain("TOP@faces@>Z", "BOTTOM@faces@X", "BOTTOM@faces@ Date: Fri, 25 Sep 2020 18:43:05 +0200 Subject: [PATCH 62/70] Relaxed test criteria --- tests/test_assembly.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 28220d97f..a2b0c0e89 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -177,7 +177,7 @@ def test_constrain(simple_assy, nested_assy): simple_assy.children[0] .loc.wrapped.Transformation() .TranslationPart() - .IsEqual(gp_XYZ(-1, 0.5, 0.5), 5e-3) + .IsEqual(gp_XYZ(-1, 0.5, 0.5), 1e-2) ) nested_assy.solve() @@ -186,5 +186,5 @@ def test_constrain(simple_assy, nested_assy): nested_assy.children[0] .loc.wrapped.Transformation() .TranslationPart() - .IsEqual(gp_XYZ(2, -4, 0.75), 5e-3) + .IsEqual(gp_XYZ(2, -4, 0.75), 1e-2) ) From 90ce94e21410e8c206ae14b6eab4e1d673fb7eb0 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 26 Sep 2020 20:44:09 +0200 Subject: [PATCH 63/70] Changed the cost to use Angle nad added LSQ cost for experiments --- cadquery/occ_impl/solver.py | 129 +++++++++++++++++++++++++++++++++--- 1 file changed, 120 insertions(+), 9 deletions(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 77e31a12c..547da45fe 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -1,8 +1,8 @@ from typing import Tuple, Union, Any, Callable, List, Optional from nptyping import NDArray as Array -from numpy import array, eye, zeros -from scipy.optimize import minimize +from numpy import array, eye, zeros, pi +from scipy.optimize import minimize, least_squares from OCP.gp import gp_Vec, gp_Dir, gp_Pnt, gp_Trsf, gp_Quaternion @@ -13,8 +13,8 @@ Constraint = Tuple[Tuple[ConstraintMarker, ...], Tuple[Optional[ConstraintMarker], ...]] NDOF = 6 -DIR_SCALING = 1e3 -DIFF_EPS = 1e-8 +DIR_SCALING = 1e4 +DIFF_EPS = 1e-9 class ConstraintSolver(object): @@ -91,10 +91,12 @@ def pt_cost(m1: gp_Pnt, m2: gp_Pnt, t1: gp_Trsf, t2: gp_Trsf) -> float: return (m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ()).SquareModulus() def dir_cost( - m1: gp_Dir, m2: gp_Dir, t1: gp_Trsf, t2: gp_Trsf, val: float = -1 + m1: gp_Dir, m2: gp_Dir, t1: gp_Trsf, t2: gp_Trsf, val: float = pi ) -> float: - return DIR_SCALING * (val - m1.Transformed(t1).Dot(m2.Transformed(t2))) ** 2 + return ( + DIR_SCALING * (val - m1.Transformed(t1).Angle(m2.Transformed(t2))) ** 2 + ) def f(x): @@ -183,6 +185,116 @@ def jac(x): return f, jac + def _costlsq( + self, + ) -> Tuple[ + Callable[[Array[(Any,), float]], Array[(Any,), float]], + Callable[[Array[(Any,), float]], Array[(Any, Any), float]], + ]: + def pt_cost(m1: gp_Pnt, m2: gp_Pnt, t1: gp_Trsf, t2: gp_Trsf) -> float: + + return (m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ()).Modulus() + + def dir_cost( + m1: gp_Dir, m2: gp_Dir, t1: gp_Trsf, t2: gp_Trsf, val: float = pi + ) -> float: + + return val - m1.Transformed(t1).Angle(m2.Transformed(t2)) + + def f(x): + + constraints = self.constraints + ne = self.ne + nc = self.nc + + rv = zeros(nc + ne * NDOF) + + transforms = [ + self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) + ] + + for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): + t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() + t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() + + for m1, m2 in zip(ms1, ms2): + if isinstance(m1, gp_Pnt): + rv[i] += pt_cost(m1, m2, t1, t2) + elif isinstance(m1, gp_Dir): + rv[i] += dir_cost(m1, m2, t1, t2) + else: + raise NotImplementedError(f"{m1,m2}") + + rv[nc:] = 1e-9 * x ** 2 + + return rv + + def jac(x): + + constraints = self.constraints + ne = self.ne + nc = self.nc + + delta = DIFF_EPS * eye(NDOF) + + rv = zeros((nc + NDOF * ne, NDOF * ne)) + + transforms = [ + self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) + ] + + transforms_delta = [ + self._build_transform(*(x[NDOF * i : NDOF * (i + 1)] + delta[j, :])) + for i in range(ne) + for j in range(NDOF) + ] + + for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): + t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() + t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() + + for m1, m2 in zip(ms1, ms2): + if isinstance(m1, gp_Pnt): + tmp = pt_cost(m1, m2, t1, t2) + + for j in range(NDOF): + + t1j = transforms_delta[k1 * NDOF + j] + t2j = transforms_delta[k2 * NDOF + j] + + if k1 not in self.locked: + tmp1 = pt_cost(m1, m2, t1j, t2) + rv[i, k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS + + if k2 not in self.locked: + tmp2 = pt_cost(m1, m2, t1, t2j) + rv[i, k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS + + elif isinstance(m1, gp_Dir): + tmp = dir_cost(m1, m2, t1, t2) + + for j in range(NDOF): + + t1j = transforms_delta[k1 * NDOF + j] + t2j = transforms_delta[k2 * NDOF + j] + + if k1 not in self.locked: + tmp1 = dir_cost(m1, m2, t1j, t2) + rv[i, k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS + + if k2 not in self.locked: + tmp2 = dir_cost(m1, m2, t1, t2j) + rv[i, k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS + else: + raise NotImplementedError(f"{m1,m2}") + + for i in range(NDOF * ne): + rv[nc + i, i] = 1e-9 + + return rv + + return f, jac + def solve(self) -> List[Location]: x0 = array([el for el in self.entities]).ravel() @@ -192,12 +304,11 @@ def solve(self) -> List[Location]: f, x0, jac=jac, - method="L-BFGS-B", - options=dict(disp=True, ftol=1e-14, maxiter=1000), + method="BFGS", + options=dict(disp=True, ftol=1e-12, maxiter=1000), ) x = res.x - print(res.message) return [ Location(self._build_transform(*x[NDOF * i : NDOF * (i + 1)])) From 2625e21a1944fe50de4943a5e694ae7df2917685 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 26 Sep 2020 20:45:22 +0200 Subject: [PATCH 64/70] Tighter tolerances on the solve --- tests/test_assembly.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_assembly.py b/tests/test_assembly.py index a2b0c0e89..7c54949f5 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -177,7 +177,7 @@ def test_constrain(simple_assy, nested_assy): simple_assy.children[0] .loc.wrapped.Transformation() .TranslationPart() - .IsEqual(gp_XYZ(-1, 0.5, 0.5), 1e-2) + .IsEqual(gp_XYZ(-1, 0.5, 0.5), 1e-6) ) nested_assy.solve() @@ -186,5 +186,5 @@ def test_constrain(simple_assy, nested_assy): nested_assy.children[0] .loc.wrapped.Transformation() .TranslationPart() - .IsEqual(gp_XYZ(2, -4, 0.75), 1e-2) + .IsEqual(gp_XYZ(2, -4, 0.75), 1e-6) ) From 012b1d6cce9359fccd3b80e2010b44ad6045b775 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 29 Sep 2020 18:24:21 +0200 Subject: [PATCH 65/70] Parameter handling --- cadquery/assembly.py | 8 ++ cadquery/occ_impl/solver.py | 171 +++++++++--------------------------- 2 files changed, 48 insertions(+), 131 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 6e98efa83..56dd12914 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -88,6 +88,8 @@ def toPOD(self) -> ConstraintPOD: else: raise ValueError(f"Unknown constraint kind {self.kind}") + rv.append(self.param) + return cast(ConstraintPOD, tuple(rv)) @@ -299,8 +301,14 @@ def constrain(self, *args, param=None): q1, q2, kind = args id1, s1 = self._query(q1) id2, s2 = self._query(q2) + elif len(args) == 4: + q1, q2, kind, param = args + id1, s1 = self._query(q1) + id2, s2 = self._query(q2) elif len(args) == 5: id1, s1, id2, s2, kind = args + elif len(args) == 6: + id1, s1, id2, s2, kind, param = args else: raise ValueError(f"Incompatibile arguments: {args}") diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 547da45fe..64c78e8d7 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -2,7 +2,7 @@ from nptyping import NDArray as Array from numpy import array, eye, zeros, pi -from scipy.optimize import minimize, least_squares +from scipy.optimize import minimize from OCP.gp import gp_Vec, gp_Dir, gp_Pnt, gp_Trsf, gp_Quaternion @@ -10,7 +10,9 @@ DOF6 = Tuple[float, float, float, float, float, float] ConstraintMarker = Union[gp_Dir, gp_Pnt] -Constraint = Tuple[Tuple[ConstraintMarker, ...], Tuple[Optional[ConstraintMarker], ...]] +Constraint = Tuple[ + Tuple[ConstraintMarker, ...], Tuple[Optional[ConstraintMarker], ...], Optional[Any] +] NDOF = 6 DIR_SCALING = 1e4 @@ -37,13 +39,14 @@ def __init__( # decompose into simple constraints for k, v in constraints: - e1, e2 = v - if e2: - for m1, m2 in zip(e1, e2): - self.constraints.append((k, ((m1,), (m2,)))) + ms1, ms2, d = v + if ms2: + for m1, m2 in zip(ms1, ms2): + self.constraints.append((k, ((m1,), (m2,), d))) else: - for m1 in e1: - self.constraints.append((k, ((m1,), (None,)))) + raise NotImplementedError( + "Single marker constraints are not implemented" + ) self.ne = len(entities) self.locked = locked @@ -86,14 +89,30 @@ def _cost( Callable[[Array[(Any,), float]], float], Callable[[Array[(Any,), float]], Array[(Any,), float]], ]: - def pt_cost(m1: gp_Pnt, m2: gp_Pnt, t1: gp_Trsf, t2: gp_Trsf) -> float: + def pt_cost( + m1: gp_Pnt, + m2: gp_Pnt, + t1: gp_Trsf, + t2: gp_Trsf, + val: Optional[float] = None, + ) -> float: + + val = 0 if val is None else val - return (m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ()).SquareModulus() + return ( + val - (m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ()).Modulus() + ) ** 2 def dir_cost( - m1: gp_Dir, m2: gp_Dir, t1: gp_Trsf, t2: gp_Trsf, val: float = pi + m1: gp_Dir, + m2: gp_Dir, + t1: gp_Trsf, + t2: gp_Trsf, + val: Optional[float] = None, ) -> float: + val = pi if val is None else val + return ( DIR_SCALING * (val - m1.Transformed(t1).Angle(m2.Transformed(t2))) ** 2 ) @@ -109,15 +128,15 @@ def f(x): self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) ] - for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): + for i, ((k1, k2), (ms1, ms2, d)) in enumerate(constraints): t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() for m1, m2 in zip(ms1, ms2): if isinstance(m1, gp_Pnt): - rv += pt_cost(m1, m2, t1, t2) + rv += pt_cost(m1, m2, t1, t2, d) elif isinstance(m1, gp_Dir): - rv += dir_cost(m1, m2, t1, t2) + rv += dir_cost(m1, m2, t1, t2, d) else: raise NotImplementedError(f"{m1,m2}") @@ -142,13 +161,13 @@ def jac(x): for j in range(NDOF) ] - for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): + for i, ((k1, k2), (ms1, ms2, d)) in enumerate(constraints): t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() for m1, m2 in zip(ms1, ms2): if isinstance(m1, gp_Pnt): - tmp = pt_cost(m1, m2, t1, t2) + tmp = pt_cost(m1, m2, t1, t2, d) for j in range(NDOF): @@ -156,15 +175,15 @@ def jac(x): t2j = transforms_delta[k2 * NDOF + j] if k1 not in self.locked: - tmp1 = pt_cost(m1, m2, t1j, t2) + tmp1 = pt_cost(m1, m2, t1j, t2, d) rv[k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS if k2 not in self.locked: - tmp2 = pt_cost(m1, m2, t1, t2j) + tmp2 = pt_cost(m1, m2, t1, t2j, d) rv[k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS elif isinstance(m1, gp_Dir): - tmp = dir_cost(m1, m2, t1, t2) + tmp = dir_cost(m1, m2, t1, t2, d) for j in range(NDOF): @@ -172,11 +191,11 @@ def jac(x): t2j = transforms_delta[k2 * NDOF + j] if k1 not in self.locked: - tmp1 = dir_cost(m1, m2, t1j, t2) + tmp1 = dir_cost(m1, m2, t1j, t2, d) rv[k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS if k2 not in self.locked: - tmp2 = dir_cost(m1, m2, t1, t2j) + tmp2 = dir_cost(m1, m2, t1, t2j, d) rv[k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS else: raise NotImplementedError(f"{m1,m2}") @@ -185,116 +204,6 @@ def jac(x): return f, jac - def _costlsq( - self, - ) -> Tuple[ - Callable[[Array[(Any,), float]], Array[(Any,), float]], - Callable[[Array[(Any,), float]], Array[(Any, Any), float]], - ]: - def pt_cost(m1: gp_Pnt, m2: gp_Pnt, t1: gp_Trsf, t2: gp_Trsf) -> float: - - return (m1.Transformed(t1).XYZ() - m2.Transformed(t2).XYZ()).Modulus() - - def dir_cost( - m1: gp_Dir, m2: gp_Dir, t1: gp_Trsf, t2: gp_Trsf, val: float = pi - ) -> float: - - return val - m1.Transformed(t1).Angle(m2.Transformed(t2)) - - def f(x): - - constraints = self.constraints - ne = self.ne - nc = self.nc - - rv = zeros(nc + ne * NDOF) - - transforms = [ - self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) - ] - - for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): - t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() - t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() - - for m1, m2 in zip(ms1, ms2): - if isinstance(m1, gp_Pnt): - rv[i] += pt_cost(m1, m2, t1, t2) - elif isinstance(m1, gp_Dir): - rv[i] += dir_cost(m1, m2, t1, t2) - else: - raise NotImplementedError(f"{m1,m2}") - - rv[nc:] = 1e-9 * x ** 2 - - return rv - - def jac(x): - - constraints = self.constraints - ne = self.ne - nc = self.nc - - delta = DIFF_EPS * eye(NDOF) - - rv = zeros((nc + NDOF * ne, NDOF * ne)) - - transforms = [ - self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne) - ] - - transforms_delta = [ - self._build_transform(*(x[NDOF * i : NDOF * (i + 1)] + delta[j, :])) - for i in range(ne) - for j in range(NDOF) - ] - - for i, ((k1, k2), (ms1, ms2)) in enumerate(constraints): - t1 = transforms[k1] if k1 not in self.locked else gp_Trsf() - t2 = transforms[k2] if k2 not in self.locked else gp_Trsf() - - for m1, m2 in zip(ms1, ms2): - if isinstance(m1, gp_Pnt): - tmp = pt_cost(m1, m2, t1, t2) - - for j in range(NDOF): - - t1j = transforms_delta[k1 * NDOF + j] - t2j = transforms_delta[k2 * NDOF + j] - - if k1 not in self.locked: - tmp1 = pt_cost(m1, m2, t1j, t2) - rv[i, k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS - - if k2 not in self.locked: - tmp2 = pt_cost(m1, m2, t1, t2j) - rv[i, k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS - - elif isinstance(m1, gp_Dir): - tmp = dir_cost(m1, m2, t1, t2) - - for j in range(NDOF): - - t1j = transforms_delta[k1 * NDOF + j] - t2j = transforms_delta[k2 * NDOF + j] - - if k1 not in self.locked: - tmp1 = dir_cost(m1, m2, t1j, t2) - rv[i, k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS - - if k2 not in self.locked: - tmp2 = dir_cost(m1, m2, t1, t2j) - rv[i, k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS - else: - raise NotImplementedError(f"{m1,m2}") - - for i in range(NDOF * ne): - rv[nc + i, i] = 1e-9 - - return rv - - return f, jac - def solve(self) -> List[Location]: x0 = array([el for el in self.entities]).ravel() From 43e8377eb35323dc24b23b81f7a47b62e1a4a651 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 30 Sep 2020 18:18:40 +0200 Subject: [PATCH 66/70] Docs update --- doc/_static/assy.png | Bin 0 -> 139192 bytes doc/_static/simple_assy.png | Bin 0 -> 21577 bytes doc/extending.rst | 24 ++++++++++++------------ doc/intro.rst | 7 +++---- doc/primer.rst | 30 ++++++++++++++++++++++-------- 5 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 doc/_static/assy.png create mode 100644 doc/_static/simple_assy.png diff --git a/doc/_static/assy.png b/doc/_static/assy.png new file mode 100644 index 0000000000000000000000000000000000000000..8ee52dba5c463769e7ba51459f7f73890324dc68 GIT binary patch literal 139192 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 Ykmc&VyU|^h0fEcJ(9EFZ@=e130b!jC0ssI2 literal 0 HcmV?d00001 diff --git a/doc/_static/simple_assy.png b/doc/_static/simple_assy.png new file mode 100644 index 0000000000000000000000000000000000000000..bee932dfbb3a400e08cbdb22d1817234c346a6cf GIT binary patch literal 21577 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").size() # returns 6 @@ -34,10 +34,10 @@ two. For example, consider this script, which creates a PythonOCC box, but then Extending CadQuery: Plugins ---------------------------- -Though you can get a lot done with PythonOCC, the code gets pretty nasty in a hurry. CadQuery shields you from -a lot of the complexity of the PythonOCC API. +Though you can get a lot done with OpenCascade, the code gets pretty nasty in a hurry. CadQuery shields you from +a lot of the complexity of the OpenCascade API. -You can get the best of both worlds by wrapping your PythonOCC script into a CadQuery plugin. +You can get the best of both worlds by wrapping your OCP script into a CadQuery plugin. A CadQuery plugin is simply a function that is attached to the CadQuery :py:meth:`cadquery.CQ` or :py:meth:`cadquery.Workplane` class. When connected, your plugin can be used in the chain just like the built-in functions. @@ -51,8 +51,8 @@ The Stack Every CadQuery object has a local stack, which contains a list of items. The items on the stack will be one of these types: - * **A CadQuery SolidReference object**, which holds a reference to a PythonOCC solid - * **A PythonOCC object**, a Vertex, Edge, Wire, Face, Shell, Solid, or Compound + * **A CadQuery SolidReference object**, which holds a reference to a OCP solid + * **A OCP object**, a Vertex, Edge, Wire, Face, Shell, Solid, or Compound The stack is available by using self.objects, and will always contain at least one object. diff --git a/doc/intro.rst b/doc/intro.rst index 22dde7d9f..5bb6642d1 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -19,7 +19,7 @@ CadQuery is an intuitive, easy-to-use Python library for building parametric 3D * Provide a non-proprietary, plain text model format that can be edited and executed with only a web browser CadQuery 2.0 is based on -`PythonOCC `_, +`OCP https://github.com/CadQuery/OCP`_, which is a set of Python bindings for the open-source `OpenCascade `_ modelling kernel. Using CadQuery, you can build fully parametric models with a very small amount of code. For example, this simple script @@ -54,8 +54,7 @@ its use in a variety of engineering and scientific applications that create 3D m If you'd like a GUI, you have a couple of options: * The Qt-based GUI `CQ-editor `_ - * As an Jupyter extension `cadquery-jupyter-extension - `_ + * As an Jupyter extension `jupyter-cadquery `_ Why CadQuery instead of OpenSCAD? @@ -70,7 +69,7 @@ Like OpenSCAD, CadQuery is an open-source, script based, parametric model genera by OCC include NURBS, splines, surface sewing, STL repair, STEP import/export, and other complex operations, in addition to the standard CSG operations supported by CGAL - 3. **Ability to import/export STEP** We think the ability to begin with a STEP model, created in a CAD package, + 3. **Ability to import/export STEP and DXF** We think the ability to begin with a STEP model, created in a CAD package, and then add parametric features is key. This is possible in OpenSCAD using STL, but STL is a lossy format 4. **Less Code and easier scripting** CadQuery scripts require less code to create most objects, because it is possible to locate diff --git a/doc/primer.rst b/doc/primer.rst index 4c6c5875d..44f960cba 100644 --- a/doc/primer.rst +++ b/doc/primer.rst @@ -168,16 +168,30 @@ This is really useful to remember when you author your own plugins. :py:meth:`c Assemblies ---------- -Simple models can be combined into complex, possibly nested, assemblies:: +Simple models can be combined into complex, possibly nested, assemblies. - part1 = Workplane().box(1,1,1) - part2 = Workplane().box(1,1,2) - part3 = Workplane().box(1,1,3) +.. image:: _static/assy.png + +A simple example could look as follows:: + + from cadquery import * + + w = 10 + d = 10 + h = 10 + + part1 = Workplane().box(2*w,2*d,h) + part2 = Workplane().box(w,d,2*h) + part3 = Workplane().box(w,d,3*h) assy = ( - Assembly(part1, Location((1,0,0))) - .add(part2, Location(1,0,0)) - .add(part3, Location(-1,0,0)) + Assembly(part1, loc=Location(Vector(-w,0,h/2))) + .add(part2, loc=Location(Vector(1.5*w,-.5*d,h/2)), color=Color(0,0,1,0.5)) + .add(part3, loc=Location(Vector(-.5*w,-.5*d,2*h)), color=Color("red")) ) -Note that the locations of the children parts are defined with respect to their parents - in the above example ``part3`` will be located at (0,0,0) in the global coordinate system. +Resulting in: + +.. image:: _static/simple_assy.png + +Note that the locations of the children parts are defined with respect to their parents - in the above example ``part3`` will be located at (-5,-5,20) in the global coordinate system. Assemblies with different colors can be created this way and exported to STEP or the native OCCT xml format. From a170cb0df6c4f7dc9ff392b07c29c85df61f53cb Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 30 Sep 2020 18:25:21 +0200 Subject: [PATCH 67/70] ftol --> gtol --- cadquery/occ_impl/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/solver.py b/cadquery/occ_impl/solver.py index 64c78e8d7..66961c06d 100644 --- a/cadquery/occ_impl/solver.py +++ b/cadquery/occ_impl/solver.py @@ -214,7 +214,7 @@ def solve(self) -> List[Location]: x0, jac=jac, method="BFGS", - options=dict(disp=True, ftol=1e-12, maxiter=1000), + options=dict(disp=True, gtol=1e-12, maxiter=1000), ) x = res.x From ba7830d40561c8fd44b087f316438c2b09c84004 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 30 Sep 2020 18:31:48 +0200 Subject: [PATCH 68/70] Do not touch the original parent --- cadquery/assembly.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index 56dd12914..d7ef37f66 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -218,8 +218,6 @@ def add(self, arg, **kwargs): self.objects[subassy.name] = subassy self.objects.update(subassy.objects) - arg.parent = self - else: assy = Assembly(arg, **kwargs) assy.parent = self From 8f62fc54ddb8cb40069f0a711688c822c5ec645f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 1 Oct 2020 08:33:03 +0200 Subject: [PATCH 69/70] Review fixes --- cadquery/assembly.py | 4 ++-- cadquery/occ_impl/shapes.py | 2 +- mypy.ini | 2 +- tests/test_assembly.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index d7ef37f66..c895260fb 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -126,11 +126,11 @@ def __init__( :return: An Assembly object. - To create an empt assembly use:: + To create an empty assembly use:: assy = Assembly(None) - To create one containt a root object:: + To create one constraint a root object:: b = Workplane().box(1,1,1) assy = Assembly(b, Location(Vector(0,0,1)), name="root") diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 55b293063..67d3f8687 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -314,7 +314,7 @@ def fix(obj: TopoDS_Shape) -> TopoDS_Shape: class Shape(object): """ - Represents a shape in the system.Wraps TopoDS_Shape. + Represents a shape in the system. Wraps TopoDS_Shape. """ wrapped: TopoDS_Shape diff --git a/mypy.ini b/mypy.ini index e6ede69d8..9eb662b60 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,4 +17,4 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-nptyping.*] -ignore_missing_imports = True \ No newline at end of file +ignore_missing_imports = True diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 7c54949f5..5c9e39128 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -69,7 +69,7 @@ def test_assembly(simple_assy, nested_assy): assert len(nested_assy.children) == 1 assert nested_assy.objects["SECOND"].parent is nested_assy - # bottm-up traversal + # bottom-up traversal kvs = list(nested_assy.traverse()) assert kvs[0][0] == "BOTTOM" From 21f7f481616cadaa253527c0936ed313bf8eba47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Thu, 1 Oct 2020 08:52:03 +0200 Subject: [PATCH 70/70] Mention assemblies in the README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 3f6d887a0..a3f15ba02 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ CadQuery is often compared to [OpenSCAD](http://www.openscad.org/). Like OpenSCA * Output high quality (loss-less) CAD formats like STEP and DXF in addition to STL and AMF. * Provide a non-proprietary, plain text model format that can be edited and executed with only a web browser. * Offer advanced modeling capabilities such as fillets, curvelinear extrudes, parametric curves and lofts. +* Build nested assemblies out of individual parts and other assemblies. ### Why this fork @@ -101,6 +102,12 @@ Thanks to @easyw for this example from the [kicad-3d-models-in-freecad project]( Circuit board generated in KiCAD +### Spindle assembly + +Thanks to @marcus7070 for this example from [here](https://github.com/marcus7070/spindle-assy-example). + + + ### 3D Printed Resin Mold Thanks to @eddieliberato for this example.