@ -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.
|
* 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.
|
* 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.
|
* 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
|
### Why this fork
|
||||||
|
|
||||||
@ -111,6 +112,12 @@ Hexidor is an expanded take on the Quoridor board game, and the development proc
|
|||||||
|
|
||||||
<img src="https://bruceisonfire.net/wp-content/uploads/2020/04/16-945x709.jpg" alt="Hexidor Board Game" width="400"/>
|
<img src="https://bruceisonfire.net/wp-content/uploads/2020/04/16-945x709.jpg" alt="Hexidor Board Game" width="400"/>
|
||||||
|
|
||||||
|
### Spindle assembly
|
||||||
|
|
||||||
|
Thanks to @marcus7070 for this example from [here](https://github.com/marcus7070/spindle-assy-example).
|
||||||
|
|
||||||
|
<img src="./doc/_static/assy.png" width="400">
|
||||||
|
|
||||||
### 3D Printed Resin Mold
|
### 3D Printed Resin Mold
|
||||||
|
|
||||||
Thanks to @eddieliberato for sharing [this example](https://jungletools.blogspot.com/2017/06/an-anti-kink-device-for-novel-high-tech.html) of an anti-kink resin mold for a cable.
|
Thanks to @eddieliberato for sharing [this example](https://jungletools.blogspot.com/2017/06/an-anti-kink-device-for-novel-high-tech.html) of an anti-kink resin mold for a cable.
|
||||||
|
@ -28,6 +28,7 @@ from .selectors import (
|
|||||||
Selector,
|
Selector,
|
||||||
)
|
)
|
||||||
from .cq import CQ, Workplane
|
from .cq import CQ, Workplane
|
||||||
|
from .assembly import Assembly, Color, Constraint
|
||||||
from . import selectors
|
from . import selectors
|
||||||
from . import plugins
|
from . import plugins
|
||||||
|
|
||||||
@ -35,6 +36,9 @@ from . import plugins
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"CQ",
|
"CQ",
|
||||||
"Workplane",
|
"Workplane",
|
||||||
|
"Assembly",
|
||||||
|
"Color",
|
||||||
|
"Constraint",
|
||||||
"plugins",
|
"plugins",
|
||||||
"selectors",
|
"selectors",
|
||||||
"Plane",
|
"Plane",
|
||||||
@ -64,4 +68,4 @@ __all__ = [
|
|||||||
"plugins",
|
"plugins",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "2.0"
|
__version__ = "2.1dev"
|
||||||
|
413
cadquery/assembly.py
Normal file
413
cadquery/assembly.py
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
from .cq import Workplane
|
||||||
|
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 (
|
||||||
|
ConstraintSolver,
|
||||||
|
ConstraintMarker,
|
||||||
|
Constraint as ConstraintPOD,
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Geometrical constraint between two shapes of an assembly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
objects: Tuple[str, ...]
|
||||||
|
args: Tuple[Shape, ...]
|
||||||
|
sublocs: Tuple[Location, ...]
|
||||||
|
kind: ConstraintKinds
|
||||||
|
param: Any
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
objects: Tuple[str, ...],
|
||||||
|
args: Tuple[Shape, ...],
|
||||||
|
sublocs: 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 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.sublocs = sublocs
|
||||||
|
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.sublocs):
|
||||||
|
|
||||||
|
arg = arg.located(loc * arg.location())
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
rv.append(self.param)
|
||||||
|
|
||||||
|
return cast(ConstraintPOD, tuple(rv))
|
||||||
|
|
||||||
|
|
||||||
|
class Assembly(object):
|
||||||
|
"""Nested assembly of Workplane and Shape objects defining their relative positions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
loc: Location
|
||||||
|
name: str
|
||||||
|
color: Optional[Color]
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
obj: AssemblyObjects
|
||||||
|
parent: Optional["Assembly"]
|
||||||
|
children: List["Assembly"]
|
||||||
|
|
||||||
|
objects: Dict[str, "Assembly"]
|
||||||
|
constraints: List[Constraint]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
obj: AssemblyObjects = None,
|
||||||
|
loc: Optional[Location] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
color: Optional[Color] = 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)
|
||||||
|
:param color: color of the added object (default: None)
|
||||||
|
:return: An Assembly object.
|
||||||
|
|
||||||
|
|
||||||
|
To create an empty assembly use::
|
||||||
|
|
||||||
|
assy = Assembly(None)
|
||||||
|
|
||||||
|
To create one constraint 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.color = color if color else None
|
||||||
|
self.parent = None
|
||||||
|
|
||||||
|
self.children = []
|
||||||
|
self.constraints = []
|
||||||
|
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(
|
||||||
|
self,
|
||||||
|
obj: "Assembly",
|
||||||
|
loc: Optional[Location] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
color: Optional[Color] = 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)
|
||||||
|
:param color: color of the added object (default: None, resulting in the color stored in the subassembly being used)
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
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
|
||||||
|
|
||||||
|
: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)
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def add(self, arg, **kwargs):
|
||||||
|
"""
|
||||||
|
add a subassembly to the current assembly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(arg, Assembly):
|
||||||
|
|
||||||
|
subassy = arg._copy()
|
||||||
|
|
||||||
|
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
|
||||||
|
self.objects.update(subassy.objects)
|
||||||
|
|
||||||
|
else:
|
||||||
|
assy = Assembly(arg, **kwargs)
|
||||||
|
assy.parent = self
|
||||||
|
|
||||||
|
self.add(assy)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
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].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) -> 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 and obj is not self:
|
||||||
|
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, name_out)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
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,
|
||||||
|
) -> "Assembly":
|
||||||
|
...
|
||||||
|
|
||||||
|
def constrain(self, *args, param=None):
|
||||||
|
"""
|
||||||
|
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) == 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}")
|
||||||
|
|
||||||
|
loc1, id1_top = self._subloc(id1)
|
||||||
|
loc2, id2_top = self._subloc(id2)
|
||||||
|
self.constraints.append(
|
||||||
|
Constraint((id1_top, id2_top), (s1, s2), (loc1, loc2), kind, param)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def solve(self) -> "Assembly":
|
||||||
|
"""
|
||||||
|
Solve the constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get all entities and number them
|
||||||
|
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
|
||||||
|
constraints = []
|
||||||
|
for c in self.constraints:
|
||||||
|
constraints.append(((ents[c.objects[0]], ents[c.objects[1]]), c.toPOD()))
|
||||||
|
|
||||||
|
# instantiate the solver
|
||||||
|
solver = ConstraintSolver(locs, constraints, locked=[lock_ix])
|
||||||
|
|
||||||
|
# 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
|
||||||
|
) -> "Assembly":
|
||||||
|
"""
|
||||||
|
save as STEP or OCCT native XML file
|
||||||
|
|
||||||
|
:param path: filepath
|
||||||
|
:param exportType: export format (deafault: None, results in format being inferred form the path)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if exportType is None:
|
||||||
|
t = path.split(".")[-1].upper()
|
||||||
|
if t in ("STEP", "XML"):
|
||||||
|
exportType = cast(ExportLiterals, t)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown extension, specify export type explicitly")
|
||||||
|
|
||||||
|
if exportType == "STEP":
|
||||||
|
exportAssembly(self, path)
|
||||||
|
elif exportType == "XML":
|
||||||
|
exportCAF(self, path)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown format: {exportType}")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, path: str) -> "Assembly":
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shapes(self) -> List[Shape]:
|
||||||
|
"""
|
||||||
|
List of Shape objects in the .obj field
|
||||||
|
"""
|
||||||
|
|
||||||
|
rv: List[Shape] = []
|
||||||
|
|
||||||
|
if isinstance(self.obj, Shape):
|
||||||
|
rv = [self.obj]
|
||||||
|
elif isinstance(self.obj, Workplane):
|
||||||
|
rv = [el for el in self.obj.vals() if isinstance(el, Shape)]
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def traverse(self) -> Iterator[Tuple[str, "Assembly"]]:
|
||||||
|
"""
|
||||||
|
Yield (name, child) pairs in a bottom-up manner
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ch in self.children:
|
||||||
|
for el in ch.traverse():
|
||||||
|
yield el
|
||||||
|
|
||||||
|
yield (self.name, self)
|
@ -4,10 +4,9 @@ A special directive for including a cq object.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
from cadquery import *
|
from cadquery import exporters
|
||||||
from cadquery import cqgi
|
from cadquery import cqgi
|
||||||
import io
|
from docutils.parsers.rst import directives, Directive
|
||||||
from docutils.parsers.rst import directives
|
|
||||||
|
|
||||||
template = """
|
template = """
|
||||||
|
|
||||||
@ -23,17 +22,23 @@ template = """
|
|||||||
template_content_indent = " "
|
template_content_indent = " "
|
||||||
|
|
||||||
|
|
||||||
def cq_directive(
|
class cq_directive(Directive):
|
||||||
name,
|
|
||||||
arguments,
|
has_content = True
|
||||||
options,
|
required_arguments = 0
|
||||||
content,
|
optional_arguments = 2
|
||||||
lineno,
|
option_spec = {
|
||||||
content_offset,
|
"height": directives.length_or_unitless,
|
||||||
block_text,
|
"width": directives.length_or_percentage_or_unitless,
|
||||||
state,
|
"align": directives.unchanged,
|
||||||
state_machine,
|
}
|
||||||
):
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
options = self.options
|
||||||
|
content = self.content
|
||||||
|
state_machine = self.state_machine
|
||||||
|
|
||||||
# only consider inline snippets
|
# only consider inline snippets
|
||||||
plot_code = "\n".join(content)
|
plot_code = "\n".join(content)
|
||||||
|
|
||||||
@ -43,12 +48,12 @@ def cq_directive(
|
|||||||
out_svg = "Your Script Did not assign call build_output() function!"
|
out_svg = "Your Script Did not assign call build_output() function!"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_s = io.StringIO()
|
|
||||||
result = cqgi.parse(plot_code).build()
|
result = cqgi.parse(plot_code).build()
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
exporters.exportShape(result.first_result.shape, "SVG", _s)
|
out_svg = exporters.getSVG(
|
||||||
out_svg = _s.getvalue()
|
exporters.toCompound(result.first_result.shape)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise result.exception
|
raise result.exception
|
||||||
|
|
||||||
@ -84,10 +89,4 @@ def setup(app):
|
|||||||
setup.config = app.config
|
setup.config = app.config
|
||||||
setup.confdir = app.confdir
|
setup.confdir = app.confdir
|
||||||
|
|
||||||
options = {
|
app.add_directive("cq_plot", cq_directive)
|
||||||
"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)
|
|
||||||
|
152
cadquery/occ_impl/assembly.py
Normal file
152
cadquery/occ_impl/assembly.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
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, 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):
|
||||||
|
"""
|
||||||
|
Wrapper for the OCCT color object Quantity_ColorRGBA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
wrapped: Quantity_ColorRGBA
|
||||||
|
|
||||||
|
@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):
|
||||||
|
|
||||||
|
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:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self) -> Optional["AssemblyProtocol"]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self) -> Optional[Color]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shapes(self) -> Iterable[Shape]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self) -> Iterable["AssemblyProtocol"]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def traverse(self) -> Iterable[Tuple[str, "AssemblyProtocol"]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def setName(l: TDF_Label, name: 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, coloredSTEP: bool = False
|
||||||
|
) -> 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()
|
||||||
|
TDataStd_Name.Set_s(top, TCollection_ExtendedString("CQ assembly"))
|
||||||
|
|
||||||
|
# add leafs and subassemblies
|
||||||
|
subassys: Dict[str, Tuple[TDF_Label, Location]] = {}
|
||||||
|
for k, v in assy.traverse():
|
||||||
|
# leaf part
|
||||||
|
lab = tool.NewShape()
|
||||||
|
tool.SetShape(lab, Compound.makeCompound(v.shapes).wrapped)
|
||||||
|
setName(lab, f"{k}_part", tool)
|
||||||
|
|
||||||
|
# assy part
|
||||||
|
subassy = tool.NewShape()
|
||||||
|
tool.AddComponent(subassy, lab, TopLoc_Location())
|
||||||
|
setName(subassy, k, tool)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
for ch in v.children:
|
||||||
|
tool.AddComponent(
|
||||||
|
subassy, subassys[ch.name][0], subassys[ch.name][1].wrapped
|
||||||
|
)
|
||||||
|
|
||||||
|
tool.AddComponent(top, subassys[assy.name][0], assy.loc.wrapped)
|
||||||
|
tool.UpdateAssemblies()
|
||||||
|
|
||||||
|
return top, doc
|
63
cadquery/occ_impl/exporters/assembly.py
Normal file
63
cadquery/occ_impl/exporters/assembly.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import os.path
|
||||||
|
|
||||||
|
from OCP.XSControl import XSControl_WorkSession
|
||||||
|
from OCP.STEPCAFControl import STEPCAFControl_Writer
|
||||||
|
from OCP.STEPControl import STEPControl_StepModelType
|
||||||
|
from OCP.IFSelect import IFSelect_ReturnStatus
|
||||||
|
from OCP.XCAFApp import XCAFApp_Application
|
||||||
|
from OCP.XmlDrivers import (
|
||||||
|
XmlDrivers_DocumentStorageDriver,
|
||||||
|
XmlDrivers_DocumentRetrievalDriver,
|
||||||
|
)
|
||||||
|
from OCP.TCollection import TCollection_ExtendedString, TCollection_AsciiString
|
||||||
|
from OCP.PCDM import PCDM_StoreStatus
|
||||||
|
|
||||||
|
from ..assembly import AssemblyProtocol, toCAF
|
||||||
|
|
||||||
|
|
||||||
|
def exportAssembly(assy: AssemblyProtocol, path: str) -> bool:
|
||||||
|
|
||||||
|
_, doc = toCAF(assy, True)
|
||||||
|
|
||||||
|
session = XSControl_WorkSession()
|
||||||
|
writer = STEPCAFControl_Writer(session, False)
|
||||||
|
writer.SetColorMode(True)
|
||||||
|
writer.SetLayerMode(True)
|
||||||
|
writer.SetNameMode(True)
|
||||||
|
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
|
||||||
|
|
||||||
|
status = writer.Write(path)
|
||||||
|
|
||||||
|
return status == IFSelect_ReturnStatus.IFSelect_RetDone
|
||||||
|
|
||||||
|
|
||||||
|
def exportCAF(assy: AssemblyProtocol, path: str) -> bool:
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
store = XmlDrivers_DocumentStorageDriver(
|
||||||
|
TCollection_ExtendedString("Copyright: Open Cascade, 2001-2002")
|
||||||
|
)
|
||||||
|
ret = XmlDrivers_DocumentRetrievalDriver()
|
||||||
|
|
||||||
|
app.DefineFormat(
|
||||||
|
TCollection_AsciiString("XmlOcaf"),
|
||||||
|
TCollection_AsciiString("Xml XCAF Document"),
|
||||||
|
TCollection_AsciiString(ext),
|
||||||
|
ret,
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.SetRequestedFolder(TCollection_ExtendedString(folder))
|
||||||
|
doc.SetRequestedName(TCollection_ExtendedString(name))
|
||||||
|
|
||||||
|
status = app.SaveAs(doc, TCollection_ExtendedString(path))
|
||||||
|
|
||||||
|
app.Close(doc)
|
||||||
|
|
||||||
|
return status == PCDM_StoreStatus.PCDM_SS_OK
|
@ -895,6 +895,11 @@ class Location(object):
|
|||||||
"""Location wrapping the low-level TopLoc_Location object t"""
|
"""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
|
@overload
|
||||||
def __init__(self, t: Vector, ax: Vector, angle: float) -> None:
|
def __init__(self, t: Vector, ax: Vector, angle: float) -> None:
|
||||||
"""Location with translation t and rotation around ax by angle
|
"""Location with translation t and rotation around ax by angle
|
||||||
@ -919,6 +924,8 @@ class Location(object):
|
|||||||
elif isinstance(t, TopLoc_Location):
|
elif isinstance(t, TopLoc_Location):
|
||||||
self.wrapped = t
|
self.wrapped = t
|
||||||
return
|
return
|
||||||
|
elif isinstance(t, gp_Trsf):
|
||||||
|
T = t
|
||||||
elif len(args) == 2:
|
elif len(args) == 2:
|
||||||
t, v = args
|
t, v = args
|
||||||
cs = gp_Ax3(v.toPnt(), t.zDir.toDir(), t.xDir.toDir())
|
cs = gp_Ax3(v.toPnt(), t.zDir.toDir(), t.xDir.toDir())
|
||||||
|
@ -314,8 +314,7 @@ def fix(obj: TopoDS_Shape) -> TopoDS_Shape:
|
|||||||
|
|
||||||
class Shape(object):
|
class Shape(object):
|
||||||
"""
|
"""
|
||||||
Represents a shape in the system.
|
Represents a shape in the system. Wraps TopoDS_Shape.
|
||||||
Wrappers the FreeCAD apiSh
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
wrapped: TopoDS_Shape
|
wrapped: TopoDS_Shape
|
||||||
@ -695,6 +694,13 @@ class Shape(object):
|
|||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def location(self) -> Location:
|
||||||
|
"""
|
||||||
|
Return the current location
|
||||||
|
"""
|
||||||
|
|
||||||
|
return Location(self.wrapped.Location())
|
||||||
|
|
||||||
def locate(self, loc: Location) -> "Shape":
|
def locate(self, loc: Location) -> "Shape":
|
||||||
"""
|
"""
|
||||||
Apply a location in absolute sense to self
|
Apply a location in absolute sense to self
|
||||||
@ -978,6 +984,28 @@ class Edge(Shape, Mixin1D):
|
|||||||
|
|
||||||
return rv
|
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:
|
def Center(self) -> Vector:
|
||||||
|
|
||||||
Properties = GProp_GProps()
|
Properties = GProp_GProps()
|
||||||
|
225
cadquery/occ_impl/solver.py
Normal file
225
cadquery/occ_impl/solver.py
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
from typing import Tuple, Union, Any, Callable, List, Optional
|
||||||
|
from nptyping import NDArray as Array
|
||||||
|
|
||||||
|
from numpy import array, eye, zeros, pi
|
||||||
|
from scipy.optimize import minimize
|
||||||
|
|
||||||
|
from OCP.gp import gp_Vec, gp_Dir, gp_Pnt, gp_Trsf, gp_Quaternion
|
||||||
|
|
||||||
|
from .geom import Location
|
||||||
|
|
||||||
|
DOF6 = Tuple[float, float, float, float, float, float]
|
||||||
|
ConstraintMarker = Union[gp_Dir, gp_Pnt]
|
||||||
|
Constraint = Tuple[
|
||||||
|
Tuple[ConstraintMarker, ...], Tuple[Optional[ConstraintMarker], ...], Optional[Any]
|
||||||
|
]
|
||||||
|
|
||||||
|
NDOF = 6
|
||||||
|
DIR_SCALING = 1e4
|
||||||
|
DIFF_EPS = 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintSolver(object):
|
||||||
|
|
||||||
|
entities: List[DOF6]
|
||||||
|
constraints: List[Tuple[Tuple[int, Optional[int]], Constraint]]
|
||||||
|
locked: List[int]
|
||||||
|
ne: int
|
||||||
|
nc: int
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
entities: List[Location],
|
||||||
|
constraints: List[Tuple[Tuple[int, int], Constraint]],
|
||||||
|
locked: List[int] = [],
|
||||||
|
):
|
||||||
|
|
||||||
|
self.entities = [self._locToDOF6(loc) for loc in entities]
|
||||||
|
self.constraints = []
|
||||||
|
|
||||||
|
# decompose into simple constraints
|
||||||
|
for k, v in constraints:
|
||||||
|
ms1, ms2, d = v
|
||||||
|
if ms2:
|
||||||
|
for m1, m2 in zip(ms1, ms2):
|
||||||
|
self.constraints.append((k, ((m1,), (m2,), d)))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Single marker constraints are not implemented"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ne = len(entities)
|
||||||
|
self.locked = locked
|
||||||
|
self.nc = len(self.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 _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.SetRotation(
|
||||||
|
gp_Quaternion(
|
||||||
|
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,
|
||||||
|
) -> 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,
|
||||||
|
val: Optional[float] = None,
|
||||||
|
) -> float:
|
||||||
|
|
||||||
|
val = 0 if val is None else val
|
||||||
|
|
||||||
|
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: Optional[float] = None,
|
||||||
|
) -> float:
|
||||||
|
|
||||||
|
val = pi if val is None else val
|
||||||
|
|
||||||
|
return (
|
||||||
|
DIR_SCALING * (val - m1.Transformed(t1).Angle(m2.Transformed(t2))) ** 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def f(x):
|
||||||
|
|
||||||
|
constraints = self.constraints
|
||||||
|
ne = self.ne
|
||||||
|
|
||||||
|
rv = 0
|
||||||
|
|
||||||
|
transforms = [
|
||||||
|
self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne)
|
||||||
|
]
|
||||||
|
|
||||||
|
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, d)
|
||||||
|
elif isinstance(m1, gp_Dir):
|
||||||
|
rv += dir_cost(m1, m2, t1, t2, d)
|
||||||
|
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, 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, d)
|
||||||
|
|
||||||
|
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, d)
|
||||||
|
rv[k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS
|
||||||
|
|
||||||
|
if k2 not in self.locked:
|
||||||
|
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, d)
|
||||||
|
|
||||||
|
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, d)
|
||||||
|
rv[k1 * NDOF + j] += (tmp1 - tmp) / DIFF_EPS
|
||||||
|
|
||||||
|
if k2 not in self.locked:
|
||||||
|
tmp2 = dir_cost(m1, m2, t1, t2j, d)
|
||||||
|
rv[k2 * NDOF + j] += (tmp2 - tmp) / DIFF_EPS
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"{m1,m2}")
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
return f, jac
|
||||||
|
|
||||||
|
def solve(self) -> List[Location]:
|
||||||
|
|
||||||
|
x0 = array([el for el in self.entities]).ravel()
|
||||||
|
f, jac = self._cost()
|
||||||
|
|
||||||
|
res = minimize(
|
||||||
|
f,
|
||||||
|
x0,
|
||||||
|
jac=jac,
|
||||||
|
method="BFGS",
|
||||||
|
options=dict(disp=True, gtol=1e-12, maxiter=1000),
|
||||||
|
)
|
||||||
|
|
||||||
|
x = res.x
|
||||||
|
|
||||||
|
return [
|
||||||
|
Location(self._build_transform(*x[NDOF * i : NDOF * (i + 1)]))
|
||||||
|
for i in range(self.ne)
|
||||||
|
]
|
@ -20,6 +20,8 @@ requirements:
|
|||||||
- ezdxf
|
- ezdxf
|
||||||
- ipython
|
- ipython
|
||||||
- typing_extensions
|
- typing_extensions
|
||||||
|
- nptyping
|
||||||
|
- scipy
|
||||||
|
|
||||||
test:
|
test:
|
||||||
requires:
|
requires:
|
||||||
|
BIN
doc/_static/assy.png
vendored
Normal file
BIN
doc/_static/assy.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 136 KiB |
BIN
doc/_static/simple_assy.png
vendored
Normal file
BIN
doc/_static/simple_assy.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
@ -185,3 +185,19 @@ as a basis for futher operations.
|
|||||||
SubtractSelector
|
SubtractSelector
|
||||||
InverseSelector
|
InverseSelector
|
||||||
StringSyntaxSelector
|
StringSyntaxSelector
|
||||||
|
|
||||||
|
Assemblies
|
||||||
|
----------
|
||||||
|
|
||||||
|
Workplane and Shape objects can be connected together into assemblies
|
||||||
|
|
||||||
|
.. currentmodule:: cadquery
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
|
||||||
|
Assembly
|
||||||
|
Assembly.add
|
||||||
|
Assembly.save
|
||||||
|
Assembly.constrain
|
||||||
|
Constraint
|
||||||
|
Color
|
||||||
|
@ -18,6 +18,8 @@ Core Classes
|
|||||||
.. autosummary::
|
.. autosummary::
|
||||||
CQ
|
CQ
|
||||||
Workplane
|
Workplane
|
||||||
|
Assembly
|
||||||
|
Constraint
|
||||||
|
|
||||||
Topological Classes
|
Topological Classes
|
||||||
----------------------
|
----------------------
|
||||||
@ -39,6 +41,7 @@ Geometry Classes
|
|||||||
Vector
|
Vector
|
||||||
Matrix
|
Matrix
|
||||||
Plane
|
Plane
|
||||||
|
Location
|
||||||
|
|
||||||
Selector Classes
|
Selector Classes
|
||||||
---------------------
|
---------------------
|
||||||
|
@ -35,12 +35,15 @@ import cadquery
|
|||||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx_autodoc_typehints",
|
||||||
"sphinx.ext.viewcode",
|
"sphinx.ext.viewcode",
|
||||||
"sphinx.ext.autosummary",
|
"sphinx.ext.autosummary",
|
||||||
"cadquery.cq_directive",
|
"cadquery.cq_directive",
|
||||||
"sphinxcadquery.sphinxcadquery",
|
"sphinxcadquery.sphinxcadquery",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
always_document_param_types = True
|
||||||
|
|
||||||
# Configure `sphinxcadquery`
|
# Configure `sphinxcadquery`
|
||||||
sphinxcadquery_include_source = True
|
sphinxcadquery_include_source = True
|
||||||
|
|
||||||
|
@ -9,14 +9,14 @@ methods:
|
|||||||
|
|
||||||
* You can load plugins others have developed. This is by far the easiest way to access other code
|
* You can load plugins others have developed. This is by far the easiest way to access other code
|
||||||
* You can define your own plugins.
|
* You can define your own plugins.
|
||||||
* You can use PythonOCC scripting directly
|
* You can use OCP scripting directly
|
||||||
|
|
||||||
|
|
||||||
Using PythonOCC Script
|
Using OpenCascade methods
|
||||||
-----------------------
|
-------------------------
|
||||||
|
|
||||||
The easiest way to extend CadQuery is to simply use PythonOCC scripting inside of your build method. Just about
|
The easiest way to extend CadQuery is to simply use OpenCascade/OCP scripting inside of your build method. Just about
|
||||||
any valid PythonOCC script will execute just fine. For example, this simple CadQuery script::
|
any valid OCP script will execute just fine. For example, this simple CadQuery script::
|
||||||
|
|
||||||
return cq.Workplane("XY").box(1.0,2.0,3.0).val()
|
return cq.Workplane("XY").box(1.0,2.0,3.0).val()
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ is actually equivalent to::
|
|||||||
|
|
||||||
return cq.Shape.cast(BRepPrimAPI_MakeBox(gp_Ax2(Vector(-0.1, -1.0, -1.5), Vector(0, 0, 1)), 1.0, 2.0, 3.0).Shape())
|
return cq.Shape.cast(BRepPrimAPI_MakeBox(gp_Ax2(Vector(-0.1, -1.0, -1.5), Vector(0, 0, 1)), 1.0, 2.0, 3.0).Shape())
|
||||||
|
|
||||||
As long as you return a valid PythonOCC Shape, you can use any PythonOCC methods you like. You can even mix and match the
|
As long as you return a valid OCP Shape, you can use any OCP methods you like. You can even mix and match the
|
||||||
two. For example, consider this script, which creates a PythonOCC box, but then uses CadQuery to select its faces::
|
two. For example, consider this script, which creates a OCP box, but then uses CadQuery to select its faces::
|
||||||
|
|
||||||
box = cq.Shape.cast(BRepPrimAPI_MakeBox(gp_Ax2(Vector(-0.1, -1.0, -1.5), Vector(0, 0, 1)), 1.0, 2.0, 3.0).Shape())
|
box = cq.Shape.cast(BRepPrimAPI_MakeBox(gp_Ax2(Vector(-0.1, -1.0, -1.5), Vector(0, 0, 1)), 1.0, 2.0, 3.0).Shape())
|
||||||
cq = Workplane(box).faces(">Z").size() # returns 6
|
cq = Workplane(box).faces(">Z").size() # returns 6
|
||||||
@ -34,10 +34,10 @@ two. For example, consider this script, which creates a PythonOCC box, but then
|
|||||||
Extending CadQuery: Plugins
|
Extending CadQuery: Plugins
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
Though you can get a lot done with PythonOCC, the code gets pretty nasty in a hurry. CadQuery shields you from
|
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 PythonOCC API.
|
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.
|
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.
|
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
|
Every CadQuery object has a local stack, which contains a list of items. The items on the stack will be
|
||||||
one of these types:
|
one of these types:
|
||||||
|
|
||||||
* **A CadQuery SolidReference object**, which holds a reference to a PythonOCC solid
|
* **A CadQuery SolidReference object**, which holds a reference to a OCP solid
|
||||||
* **A PythonOCC object**, a Vertex, Edge, Wire, Face, Shell, Solid, or Compound
|
* **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.
|
The stack is available by using self.objects, and will always contain at least one object.
|
||||||
|
|
||||||
|
@ -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
|
* 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
|
CadQuery 2.0 is based on
|
||||||
`PythonOCC <http://www.pythonocc.org/>`_,
|
`OCP https://github.com/CadQuery/OCP`_,
|
||||||
which is a set of Python bindings for the open-source `OpenCascade <http://www.opencascade.com/>`_ modelling kernel.
|
which is a set of Python bindings for the open-source `OpenCascade <http://www.opencascade.com/>`_ modelling kernel.
|
||||||
|
|
||||||
Using CadQuery, you can build fully parametric models with a very small amount of code. For example, this simple script
|
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:
|
If you'd like a GUI, you have a couple of options:
|
||||||
|
|
||||||
* The Qt-based GUI `CQ-editor <https://github.com/CadQuery/CQ-editor>`_
|
* The Qt-based GUI `CQ-editor <https://github.com/CadQuery/CQ-editor>`_
|
||||||
* As an Jupyter extension `cadquery-jupyter-extension
|
* As an Jupyter extension `jupyter-cadquery <https://github.com/bernhard-42/jupyter-cadquery>`_
|
||||||
<https://github.com/bernhard-42/cadquery-jupyter-extension>`_
|
|
||||||
|
|
||||||
|
|
||||||
Why CadQuery instead of OpenSCAD?
|
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,
|
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
|
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
|
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
|
4. **Less Code and easier scripting** CadQuery scripts require less code to create most objects, because it is possible to locate
|
||||||
|
@ -165,3 +165,33 @@ 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.
|
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.
|
||||||
|
|
||||||
|
.. 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, 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"))
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -8,8 +8,9 @@ dependencies:
|
|||||||
- ipython
|
- ipython
|
||||||
- ocp
|
- ocp
|
||||||
- pyparsing
|
- pyparsing
|
||||||
- sphinx=2.4
|
- sphinx=3.2.1
|
||||||
- sphinx_rtd_theme
|
- sphinx_rtd_theme
|
||||||
|
- sphinx-autodoc-typehints
|
||||||
- black
|
- black
|
||||||
- codecov
|
- codecov
|
||||||
- pytest
|
- pytest
|
||||||
@ -17,6 +18,8 @@ dependencies:
|
|||||||
- ezdxf
|
- ezdxf
|
||||||
- ipython
|
- ipython
|
||||||
- typing_extensions
|
- typing_extensions
|
||||||
|
- nptyping
|
||||||
|
- scipy
|
||||||
- pip
|
- pip
|
||||||
- pip:
|
- pip:
|
||||||
- "--editable=."
|
- "--editable=."
|
||||||
|
9
mypy.ini
9
mypy.ini
@ -9,3 +9,12 @@ ignore_missing_imports = True
|
|||||||
|
|
||||||
[mypy-IPython.*]
|
[mypy-IPython.*]
|
||||||
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
|
||||||
|
190
tests/test_assembly.py
Normal file
190
tests/test_assembly.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
|
||||||
|
import cadquery as cq
|
||||||
|
from cadquery.occ_impl.exporters.assembly import exportAssembly, exportCAF
|
||||||
|
|
||||||
|
from OCP.gp import gp_XYZ
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def simple_assy():
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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, 1)
|
||||||
|
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, 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
|
||||||
|
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
|
||||||
|
assert nested_assy.objects["SECOND"].parent is nested_assy
|
||||||
|
|
||||||
|
# bottom-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(nested_assy):
|
||||||
|
|
||||||
|
exportAssembly(nested_assy, "nested.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()) == (0, 4, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_export(simple_assy):
|
||||||
|
|
||||||
|
exportCAF(simple_assy, "assy.xml")
|
||||||
|
|
||||||
|
# only sanity check for now
|
||||||
|
assert os.path.exists("assy.xml")
|
||||||
|
|
||||||
|
|
||||||
|
def test_save(simple_assy, nested_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")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
simple_assy.save("simple.dxf")
|
||||||
|
|
||||||
|
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(), "Plane"
|
||||||
|
)
|
||||||
|
simple_assy.constrain(
|
||||||
|
simple_assy.name, b1.Faces()[0], subassy2.name, b3.faces("<Z").val(), "Axis"
|
||||||
|
)
|
||||||
|
simple_assy.constrain(
|
||||||
|
subassy1.name,
|
||||||
|
b2.faces(">Z").val(),
|
||||||
|
subassy2.name,
|
||||||
|
b3.faces("<Z").val(),
|
||||||
|
"Point",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(simple_assy.constraints) == 3
|
||||||
|
|
||||||
|
nested_assy.constrain("TOP@faces@>Z", "BOTTOM@faces@<Z", "Plane")
|
||||||
|
nested_assy.constrain("TOP@faces@>X", "BOTTOM@faces@<X", "Axis")
|
||||||
|
|
||||||
|
assert len(nested_assy.constraints) == 2
|
||||||
|
|
||||||
|
constraint = nested_assy.constraints[0]
|
||||||
|
|
||||||
|
assert constraint.objects == ("TOP", "SECOND")
|
||||||
|
assert (
|
||||||
|
constraint.sublocs[0]
|
||||||
|
.wrapped.Transformation()
|
||||||
|
.TranslationPart()
|
||||||
|
.IsEqual(gp_XYZ(), 1e-9)
|
||||||
|
)
|
||||||
|
assert constraint.sublocs[1].wrapped.IsEqual(
|
||||||
|
nested_assy.objects["BOTTOM"].loc.wrapped
|
||||||
|
)
|
||||||
|
|
||||||
|
simple_assy.solve()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
simple_assy.loc.wrapped.Transformation()
|
||||||
|
.TranslationPart()
|
||||||
|
.IsEqual(gp_XYZ(2, -5, 0), 1e-9)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
simple_assy.children[0]
|
||||||
|
.loc.wrapped.Transformation()
|
||||||
|
.TranslationPart()
|
||||||
|
.IsEqual(gp_XYZ(-1, 0.5, 0.5), 1e-6)
|
||||||
|
)
|
||||||
|
|
||||||
|
nested_assy.solve()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
nested_assy.children[0]
|
||||||
|
.loc.wrapped.Transformation()
|
||||||
|
.TranslationPart()
|
||||||
|
.IsEqual(gp_XYZ(2, -4, 0.75), 1e-6)
|
||||||
|
)
|
@ -2,7 +2,7 @@
|
|||||||
import math
|
import math
|
||||||
import unittest
|
import unittest
|
||||||
from tests import BaseTest
|
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 OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
|
||||||
|
|
||||||
from cadquery import *
|
from cadquery import *
|
||||||
@ -408,6 +408,16 @@ class TestCadObjects(BaseTest):
|
|||||||
angle = loc2.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG
|
angle = loc2.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG
|
||||||
self.assertAlmostEqual(45, angle)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -3609,3 +3609,19 @@ class TestCadQuery(BaseTest):
|
|||||||
|
|
||||||
self.assertAlmostEqual(T3.TranslationPart().X(), r, 6)
|
self.assertAlmostEqual(T3.TranslationPart().X(), r, 6)
|
||||||
self.assertAlmostEqual(T4.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 = ell.normal()
|
||||||
|
|
||||||
|
self.assertTupleAlmostEquals(n.toTuple(), (0, 0, 1), 6)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
edge = Workplane().rect(1, 2).edges().val()
|
||||||
|
n = edge.normal()
|
||||||
|
Reference in New Issue
Block a user