Sketch support (#879)
Squashed commit of many changes, see PR #879 for full details.
This commit is contained in:
@ -14,29 +14,7 @@ resources:
|
||||
name: CadQuery/conda-packages
|
||||
endpoint: CadQuery
|
||||
|
||||
jobs:
|
||||
- template: conda-build.yml@templates
|
||||
parameters:
|
||||
name: Linux_37
|
||||
vmImage: 'ubuntu-18.04'
|
||||
py_maj: 3
|
||||
py_min: 7
|
||||
|
||||
- template: conda-build.yml@templates
|
||||
parameters:
|
||||
name: macOS_37
|
||||
vmImage: 'macOS-10.15'
|
||||
py_maj: 3
|
||||
py_min: 7
|
||||
|
||||
- template: conda-build.yml@templates
|
||||
parameters:
|
||||
name: Windows_37
|
||||
vmImage: 'vs2017-win2016'
|
||||
py_maj: 3
|
||||
py_min: 7
|
||||
conda_bld: 3.18
|
||||
|
||||
jobs:
|
||||
- template: conda-build.yml@templates
|
||||
parameters:
|
||||
name: Linux_38
|
||||
|
||||
@ -27,6 +27,7 @@ from .selectors import (
|
||||
StringSyntaxSelector,
|
||||
Selector,
|
||||
)
|
||||
from .sketch import Sketch
|
||||
from .cq import CQ, Workplane
|
||||
from .assembly import Assembly, Color, Constraint
|
||||
from . import selectors
|
||||
@ -66,6 +67,7 @@ __all__ = [
|
||||
"StringSyntaxSelector",
|
||||
"Selector",
|
||||
"plugins",
|
||||
"Sketch",
|
||||
]
|
||||
|
||||
__version__ = "2.1"
|
||||
|
||||
210
cadquery/cq.py
210
cadquery/cq.py
@ -46,6 +46,7 @@ from .occ_impl.shapes import (
|
||||
Solid,
|
||||
Compound,
|
||||
sortWiresByBuildOrder,
|
||||
wiresToFaces,
|
||||
)
|
||||
|
||||
from .occ_impl.exporters.svg import getSVG, exportSVG
|
||||
@ -59,7 +60,9 @@ from .selectors import (
|
||||
StringSyntaxSelector,
|
||||
)
|
||||
|
||||
CQObject = Union[Vector, Location, Shape]
|
||||
from .sketch import Sketch
|
||||
|
||||
CQObject = Union[Vector, Location, Shape, Sketch]
|
||||
VectorLike = Union[Tuple[float, float], Tuple[float, float, float], Vector]
|
||||
|
||||
T = TypeVar("T", bound="Workplane")
|
||||
@ -489,7 +492,9 @@ class Workplane(object):
|
||||
:rtype TopoDS_Shape or a subclass
|
||||
"""
|
||||
|
||||
return self.val().wrapped
|
||||
v = self.val()
|
||||
|
||||
return v._faces if isinstance(v, Sketch) else v.wrapped
|
||||
|
||||
def workplane(
|
||||
self: T,
|
||||
@ -2459,6 +2464,8 @@ class Workplane(object):
|
||||
for o in self.objects:
|
||||
if isinstance(o, (Vector, Shape)):
|
||||
pnts.append(loc.inverse * Location(plane, o.Center()))
|
||||
elif isinstance(o, Sketch):
|
||||
pnts.append(loc.inverse * Location(plane, o._faces.Center()))
|
||||
else:
|
||||
pnts.append(o)
|
||||
|
||||
@ -2951,9 +2958,7 @@ class Workplane(object):
|
||||
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
|
||||
:return: a CQ object with the resulting solid selected.
|
||||
"""
|
||||
# group wires together into faces based on which ones are inside the others
|
||||
# result is a list of lists
|
||||
wireSets = sortWiresByBuildOrder(self.ctx.popPendingWires())
|
||||
faces = self._getFaces()
|
||||
|
||||
# compute extrusion vector and extrude
|
||||
eDir = self.plane.zDir.multiply(distance)
|
||||
@ -2966,9 +2971,9 @@ class Workplane(object):
|
||||
# underlying cad kernel can only handle simple bosses-- we'll aggregate them if there
|
||||
# are multiple sets
|
||||
shapes: List[Shape] = []
|
||||
for ws in wireSets:
|
||||
for f in faces:
|
||||
thisObj = Solid.extrudeLinearWithRotation(
|
||||
ws[0], ws[1:], self.plane.origin, eDir, angleDegrees
|
||||
f, self.plane.origin, eDir, angleDegrees
|
||||
)
|
||||
shapes.append(thisObj)
|
||||
|
||||
@ -3452,19 +3457,16 @@ class Workplane(object):
|
||||
|
||||
see :py:meth:`cutBlind` to cut material to a limited depth
|
||||
"""
|
||||
wires = self.ctx.popPendingWires()
|
||||
solidRef = self.findSolid()
|
||||
|
||||
rv = []
|
||||
for solid in solidRef.Solids():
|
||||
s = solid.dprism(None, wires, thruAll=True, additive=False, taper=-taper)
|
||||
s = solidRef.dprism(
|
||||
None, self._getFaces(), thruAll=True, additive=False, taper=-taper
|
||||
)
|
||||
|
||||
if clean:
|
||||
s = s.clean()
|
||||
if clean:
|
||||
s = s.clean()
|
||||
|
||||
rv.append(s)
|
||||
|
||||
return self.newObject(rv)
|
||||
return self.newObject([s])
|
||||
|
||||
def loft(
|
||||
self: T, filled: bool = True, ruled: bool = False, combine: bool = True
|
||||
@ -3473,7 +3475,14 @@ class Workplane(object):
|
||||
Make a lofted solid, through the set of wires.
|
||||
:return: a CQ object containing the created loft
|
||||
"""
|
||||
wiresToLoft = self.ctx.popPendingWires()
|
||||
|
||||
if self.ctx.pendingWires:
|
||||
wiresToLoft = self.ctx.popPendingWires()
|
||||
else:
|
||||
wiresToLoft = [f.outerWire() for f in self._getFaces()]
|
||||
|
||||
if not wiresToLoft:
|
||||
raise ValueError("Nothing to loft")
|
||||
|
||||
r: Shape = Solid.makeLoft(wiresToLoft, ruled)
|
||||
|
||||
@ -3486,6 +3495,22 @@ class Workplane(object):
|
||||
|
||||
return self.newObject([r])
|
||||
|
||||
def _getFaces(self) -> List[Face]:
|
||||
"""
|
||||
Convert pending wires or sketches to faces for subsequent operation
|
||||
"""
|
||||
|
||||
rv: List[Face] = []
|
||||
|
||||
for el in self.objects:
|
||||
if isinstance(el, Sketch):
|
||||
rv.extend(el)
|
||||
|
||||
if not rv:
|
||||
rv.extend(wiresToFaces(self.ctx.popPendingWires()))
|
||||
|
||||
return rv
|
||||
|
||||
def _extrude(
|
||||
self,
|
||||
distance: Optional[float] = None,
|
||||
@ -3493,7 +3518,7 @@ class Workplane(object):
|
||||
taper: Optional[float] = None,
|
||||
upToFace: Optional[Union[int, Face]] = None,
|
||||
additive: bool = True,
|
||||
) -> Compound:
|
||||
) -> Union[Solid, Compound]:
|
||||
"""
|
||||
Make a prismatic solid from the existing set of pending wires.
|
||||
|
||||
@ -3508,13 +3533,13 @@ class Workplane(object):
|
||||
It is the basis for cutBlind, extrude, cutThruAll, and all similar methods.
|
||||
"""
|
||||
|
||||
def getFacesList(eDir, direction, both=False):
|
||||
def getFacesList(face, eDir, direction, both=False):
|
||||
"""
|
||||
Utility function to make the code further below more clean and tidy
|
||||
Performs some test and raise appropriate error when no Faces are found for extrusion
|
||||
"""
|
||||
facesList = self.findSolid().facesIntersectedByLine(
|
||||
ws[0].Center(), eDir, direction=direction
|
||||
face.Center(), eDir, direction=direction
|
||||
)
|
||||
if len(facesList) == 0 and both:
|
||||
raise ValueError(
|
||||
@ -3525,7 +3550,7 @@ class Workplane(object):
|
||||
# if we don't find faces in the workplane normal direction we try the other
|
||||
# direction (as the user might have created a workplane with wrong orientation)
|
||||
facesList = self.findSolid().facesIntersectedByLine(
|
||||
ws[0].Center(), eDir.multiply(-1.0), direction=direction
|
||||
face.Center(), eDir.multiply(-1.0), direction=direction
|
||||
)
|
||||
if len(facesList) == 0:
|
||||
raise ValueError(
|
||||
@ -3533,10 +3558,13 @@ class Workplane(object):
|
||||
)
|
||||
return facesList
|
||||
|
||||
# group wires together into faces based on which ones are inside the others
|
||||
# result is a list of lists
|
||||
# process sketches or pending wires
|
||||
faces = self._getFaces()
|
||||
|
||||
wireSets = sortWiresByBuildOrder(self.ctx.popPendingWires())
|
||||
# check for nested geometry and tapered extrusion
|
||||
for face in faces:
|
||||
if taper and face.innerWires():
|
||||
raise ValueError("Inner wires not allowed with tapered extrusion")
|
||||
|
||||
# compute extrusion vector and extrude
|
||||
if upToFace is not None:
|
||||
@ -3544,31 +3572,18 @@ class Workplane(object):
|
||||
elif distance is not None:
|
||||
eDir = self.plane.zDir.multiply(distance)
|
||||
|
||||
if additive:
|
||||
direction = "AlongAxis"
|
||||
else:
|
||||
direction = "Opposite"
|
||||
|
||||
# one would think that fusing faces into a compound and then extruding would work,
|
||||
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
|
||||
# but then cutting it from the main solid fails with BRep_NotDone.
|
||||
# the work around is to extrude each and then join the resulting solids, which seems to work
|
||||
|
||||
# underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are
|
||||
# multiple sets
|
||||
thisObj: Union[Solid, Compound]
|
||||
direction = "AlongAxis" if additive else "Opposite"
|
||||
taper = 0.0 if taper is None else taper
|
||||
|
||||
toFuse = []
|
||||
taper = 0.0 if taper is None else taper
|
||||
baseSolid = None
|
||||
|
||||
for ws in wireSets:
|
||||
if upToFace is not None:
|
||||
baseSolid = self.findSolid() if baseSolid is None else thisObj
|
||||
if upToFace is not None:
|
||||
res = self.findSolid()
|
||||
for face in faces:
|
||||
if isinstance(upToFace, int):
|
||||
facesList = getFacesList(eDir, direction, both=both)
|
||||
facesList = getFacesList(face, eDir, direction, both=both)
|
||||
if (
|
||||
baseSolid.isInside(ws[0].Center())
|
||||
res.isInside(face.outerWire().Center())
|
||||
and additive
|
||||
and upToFace == 0
|
||||
):
|
||||
@ -3578,42 +3593,33 @@ class Workplane(object):
|
||||
else:
|
||||
limitFace = upToFace
|
||||
|
||||
thisObj = Solid.dprism(
|
||||
baseSolid,
|
||||
Face.makeFromWires(ws[0]),
|
||||
ws,
|
||||
taper=taper,
|
||||
upToFace=limitFace,
|
||||
additive=additive,
|
||||
res = res.dprism(
|
||||
None, [face], taper=taper, upToFace=limitFace, additive=additive,
|
||||
)
|
||||
|
||||
if both:
|
||||
facesList2 = getFacesList(eDir.multiply(-1.0), direction, both=both)
|
||||
facesList2 = getFacesList(
|
||||
face, eDir.multiply(-1.0), direction, both=both
|
||||
)
|
||||
limitFace2 = facesList2[upToFace]
|
||||
thisObj2 = Solid.dprism(
|
||||
self.findSolid(),
|
||||
Face.makeFromWires(ws[0]),
|
||||
ws,
|
||||
res = res.dprism(
|
||||
None,
|
||||
[face],
|
||||
taper=taper,
|
||||
upToFace=limitFace2,
|
||||
additive=additive,
|
||||
)
|
||||
thisObj = Compound.makeCompound([thisObj, thisObj2])
|
||||
toFuse = [thisObj]
|
||||
elif taper != 0.0:
|
||||
thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper=taper)
|
||||
toFuse.append(thisObj)
|
||||
else:
|
||||
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir, taper=taper)
|
||||
toFuse.append(thisObj)
|
||||
|
||||
else:
|
||||
for face in faces:
|
||||
res = Solid.extrudeLinear(face, eDir, taper=taper)
|
||||
toFuse.append(res)
|
||||
|
||||
if both:
|
||||
thisObj = Solid.extrudeLinear(
|
||||
ws[0], ws[1:], eDir.multiply(-1.0), taper=taper
|
||||
)
|
||||
toFuse.append(thisObj)
|
||||
res = Solid.extrudeLinear(face, eDir.multiply(-1.0), taper=taper)
|
||||
toFuse.append(res)
|
||||
|
||||
return Compound.makeCompound(toFuse)
|
||||
return res if upToFace is not None else Compound.makeCompound(toFuse)
|
||||
|
||||
def _revolve(
|
||||
self, angleDegrees: float, axisStart: VectorLike, axisEnd: VectorLike
|
||||
@ -3631,15 +3637,11 @@ class Workplane(object):
|
||||
|
||||
This method is a utility method, primarily for plugin and internal use.
|
||||
"""
|
||||
# Get the wires to be revolved
|
||||
wireSets = sortWiresByBuildOrder(self.ctx.popPendingWires())
|
||||
|
||||
# Revolve the wires, make a compound out of them and then fuse them
|
||||
# Revolve, make a compound out of them and then fuse them
|
||||
toFuse = []
|
||||
for ws in wireSets:
|
||||
thisObj = Solid.revolve(
|
||||
ws[0], ws[1:], angleDegrees, Vector(axisStart), Vector(axisEnd)
|
||||
)
|
||||
for f in self._getFaces():
|
||||
thisObj = Solid.revolve(f, angleDegrees, Vector(axisStart), Vector(axisEnd))
|
||||
toFuse.append(thisObj)
|
||||
|
||||
return Compound.makeCompound(toFuse)
|
||||
@ -3685,11 +3687,8 @@ class Workplane(object):
|
||||
mode = wire
|
||||
|
||||
if not multisection:
|
||||
wireSets = sortWiresByBuildOrder(self.ctx.popPendingWires())
|
||||
for ws in wireSets:
|
||||
thisObj = Solid.sweep(
|
||||
ws[0], ws[1:], p, makeSolid, isFrenet, mode, transition
|
||||
)
|
||||
for f in self._getFaces():
|
||||
thisObj = Solid.sweep(f, p, makeSolid, isFrenet, mode, transition)
|
||||
toFuse.append(thisObj)
|
||||
else:
|
||||
sections = self.ctx.popPendingWires()
|
||||
@ -4230,6 +4229,55 @@ class Workplane(object):
|
||||
|
||||
return self.newObject(rv)
|
||||
|
||||
def _locs(self: T) -> List[Location]:
|
||||
"""
|
||||
Convert items on the stack into locations.
|
||||
"""
|
||||
|
||||
plane = self.plane
|
||||
locs: List[Location] = []
|
||||
|
||||
for obj in self.objects:
|
||||
if isinstance(obj, (Vector, Shape)):
|
||||
locs.append(Location(plane, obj.Center()))
|
||||
elif isinstance(obj, Location):
|
||||
locs.append(obj)
|
||||
if not locs:
|
||||
locs.append(self.plane.location)
|
||||
|
||||
return locs
|
||||
|
||||
def sketch(self: T) -> Sketch:
|
||||
"""
|
||||
Initialize and return a sketch
|
||||
|
||||
:return: Sketch object with the current workplane as a parent.
|
||||
"""
|
||||
|
||||
parent = self.newObject([])
|
||||
|
||||
rv = Sketch(parent=parent, locs=self._locs())
|
||||
parent.objects.append(rv)
|
||||
|
||||
return rv
|
||||
|
||||
def placeSketch(self: T, *sketches: Sketch) -> T:
|
||||
"""
|
||||
Place the provided sketch(es) based on the current items on the stack.
|
||||
|
||||
:return: Workplane object with the sketch added.
|
||||
"""
|
||||
|
||||
rv = []
|
||||
|
||||
for s in sketches:
|
||||
s_new = s.copy()
|
||||
s_new.locs = self._locs()
|
||||
|
||||
rv.append(s_new)
|
||||
|
||||
return self.newObject(rv)
|
||||
|
||||
def _repr_javascript_(self) -> Any:
|
||||
"""
|
||||
Special method for rendering current object in a jupyter notebook
|
||||
|
||||
@ -9,7 +9,7 @@ from pathlib import Path
|
||||
from uuid import uuid1 as uuid
|
||||
from textwrap import indent
|
||||
|
||||
from cadquery import exporters, Assembly, Compound, Color
|
||||
from cadquery import exporters, Assembly, Compound, Color, Sketch
|
||||
from cadquery import cqgi
|
||||
from cadquery.occ_impl.jupyter_tools import (
|
||||
toJSON,
|
||||
@ -308,6 +308,8 @@ class cq_directive_vtk(Directive):
|
||||
|
||||
if isinstance(shape, Assembly):
|
||||
assy = shape
|
||||
elif isinstance(shape, Sketch):
|
||||
assy = Assembly(shape._faces, color=Color(*DEFAULT_COLOR))
|
||||
else:
|
||||
assy = Assembly(shape, color=Color(*DEFAULT_COLOR))
|
||||
else:
|
||||
|
||||
405
cadquery/hull.py
Normal file
405
cadquery/hull.py
Normal file
@ -0,0 +1,405 @@
|
||||
from typing import List, Tuple, Union, Iterable, Set
|
||||
from math import pi, sin, cos, atan2, sqrt, inf, degrees
|
||||
from numpy import lexsort, argmin, argmax
|
||||
|
||||
from .occ_impl.shapes import Edge, Wire
|
||||
from .occ_impl.geom import Vector
|
||||
|
||||
|
||||
"""
|
||||
Convex hull for line segments and circular arcs based on
|
||||
Yue, Y., Murray, J. L., Corney, J. R., & Clark, D. E. R. (1999).
|
||||
Convex hull of a planar set of straight and circular line segments. Engineering Computations.
|
||||
|
||||
"""
|
||||
|
||||
Arcs = List["Arc"]
|
||||
Points = List["Point"]
|
||||
Entity = Union["Arc", "Point"]
|
||||
Hull = List[Union["Arc", "Point", "Segment"]]
|
||||
|
||||
|
||||
class Point:
|
||||
|
||||
x: float
|
||||
y: float
|
||||
|
||||
def __init__(self, x: float, y: float):
|
||||
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"( {self.x},{self.y} )"
|
||||
|
||||
def __hash__(self):
|
||||
|
||||
return hash((self.x, self.y))
|
||||
|
||||
def __eq__(self, other):
|
||||
|
||||
return (self.x, self.y) == (other.x, other.y)
|
||||
|
||||
|
||||
class Segment:
|
||||
|
||||
a: Point
|
||||
b: Point
|
||||
|
||||
def __init__(self, a: Point, b: Point):
|
||||
|
||||
self.a = a
|
||||
self.b = b
|
||||
|
||||
|
||||
class Arc:
|
||||
|
||||
c: Point
|
||||
s: Point
|
||||
e: Point
|
||||
r: float
|
||||
a1: float
|
||||
a2: float
|
||||
ac: float
|
||||
|
||||
def __init__(self, c: Point, r: float, a1: float, a2: float):
|
||||
|
||||
self.c = c
|
||||
self.r = r
|
||||
self.a1 = a1
|
||||
self.a2 = a2
|
||||
|
||||
self.s = Point(r * cos(a1), r * sin(a1))
|
||||
self.e = Point(r * cos(a2), r * sin(a2))
|
||||
self.ac = 2 * pi - (a1 - a2)
|
||||
|
||||
|
||||
def atan2p(x, y):
|
||||
|
||||
rv = atan2(y, x)
|
||||
|
||||
if rv < 0:
|
||||
rv = (2 * pi + rv) % (2 * pi)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def convert_and_validate(edges: Iterable[Edge]) -> Tuple[List[Arc], List[Point]]:
|
||||
|
||||
arcs: Set[Arc] = set()
|
||||
points: Set[Point] = set()
|
||||
|
||||
for e in edges:
|
||||
gt = e.geomType()
|
||||
|
||||
if gt == "LINE":
|
||||
p1 = e.startPoint()
|
||||
p2 = e.endPoint()
|
||||
|
||||
points.update((Point(p1.x, p1.y), Point(p2.x, p2.y)))
|
||||
|
||||
elif gt == "CIRCLE":
|
||||
c = e.arcCenter()
|
||||
r = e.radius()
|
||||
a1, a2 = e._bounds()
|
||||
|
||||
arcs.add(Arc(Point(c.x, c.y), r, a1, a2))
|
||||
|
||||
else:
|
||||
raise ValueError("Unsupported geometry {gt}")
|
||||
|
||||
return list(arcs), list(points)
|
||||
|
||||
|
||||
def select_lowest_point(points: Points) -> Tuple[Point, int]:
|
||||
|
||||
x = []
|
||||
y = []
|
||||
|
||||
for p in points:
|
||||
x.append(p.x)
|
||||
y.append(p.y)
|
||||
|
||||
# select the lowest point
|
||||
ixs = lexsort((x, y))
|
||||
|
||||
return points[ixs[0]], ixs[0]
|
||||
|
||||
|
||||
def select_lowest_arc(arcs: Arcs) -> Tuple[Point, Arc]:
|
||||
|
||||
x = []
|
||||
y = []
|
||||
|
||||
for a in arcs:
|
||||
|
||||
if a.a1 < 1.5 * pi and a.a2 > 1.5 * pi:
|
||||
x.append(a.c.x)
|
||||
y.append(a.c.y - a.r)
|
||||
else:
|
||||
p, _ = select_lowest_point([a.s, a.e])
|
||||
x.append(p.x)
|
||||
y.append(p.y)
|
||||
|
||||
ixs = lexsort((x, y))
|
||||
|
||||
return Point(x[ixs[0]], y[ixs[0]]), arcs[ixs[0]]
|
||||
|
||||
|
||||
def select_lowest(arcs: Arcs, points: Points) -> Entity:
|
||||
|
||||
rv: Entity
|
||||
|
||||
p_lowest = select_lowest_point(points) if points else None
|
||||
a_lowest = select_lowest_arc(arcs) if arcs else None
|
||||
|
||||
if p_lowest is None and a_lowest:
|
||||
rv = a_lowest[1]
|
||||
elif p_lowest is not None and a_lowest is None:
|
||||
rv = p_lowest[0]
|
||||
elif p_lowest and a_lowest:
|
||||
_, ix = select_lowest_point([p_lowest[0], a_lowest[0]])
|
||||
rv = p_lowest[0] if ix == 0 else a_lowest[1]
|
||||
else:
|
||||
raise ValueError("No entities specified")
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def pt_pt(p1: Point, p2: Point) -> Tuple[float, Segment]:
|
||||
|
||||
angle = 0
|
||||
|
||||
dx, dy = p2.x - p1.x, p2.y - p1.y
|
||||
|
||||
if (dx, dy) != (0, 0):
|
||||
angle = atan2p(dx, dy)
|
||||
|
||||
return angle, Segment(p1, p2)
|
||||
|
||||
|
||||
def _pt_arc(p: Point, a: Arc) -> Tuple[float, float, float, float]:
|
||||
|
||||
x, y = p.x, p.y
|
||||
|
||||
r = a.r
|
||||
xc, yc = a.c.x, a.c.y
|
||||
dx, dy = x - xc, y - yc
|
||||
l = sqrt(dx ** 2 + dy ** 2)
|
||||
|
||||
x1 = r ** 2 / l ** 2 * dx - r / l ** 2 * sqrt(l ** 2 - r ** 2) * dy + xc
|
||||
y1 = r ** 2 / l ** 2 * dy + r / l ** 2 * sqrt(l ** 2 - r ** 2) * dx + yc
|
||||
x2 = r ** 2 / l ** 2 * dx + r / l ** 2 * sqrt(l ** 2 - r ** 2) * dy + xc
|
||||
y2 = r ** 2 / l ** 2 * dy - r / l ** 2 * sqrt(l ** 2 - r ** 2) * dx + yc
|
||||
|
||||
return x1, y1, x2, y2
|
||||
|
||||
|
||||
def pt_arc(p: Point, a: Arc) -> Tuple[float, Segment]:
|
||||
|
||||
x, y = p.x, p.y
|
||||
x1, y1, x2, y2 = _pt_arc(p, a)
|
||||
|
||||
angles = atan2p(x1 - x, y1 - y), atan2p(x2 - x, y2 - y)
|
||||
points = Point(x1, y1), Point(x2, y2)
|
||||
ix = int(argmin(angles))
|
||||
|
||||
return angles[ix], Segment(p, points[ix])
|
||||
|
||||
|
||||
def arc_pt(a: Arc, p: Point) -> Tuple[float, Segment]:
|
||||
|
||||
x, y = p.x, p.y
|
||||
x1, y1, x2, y2 = _pt_arc(p, a)
|
||||
|
||||
angles = atan2p(x - x1, y - y1), atan2p(x - x2, y - y2)
|
||||
points = Point(x1, y1), Point(x2, y2)
|
||||
|
||||
ix = int(argmax(angles))
|
||||
|
||||
return angles[ix], Segment(points[ix], p)
|
||||
|
||||
|
||||
def arc_arc(a1: Arc, a2: Arc) -> Tuple[float, Segment]:
|
||||
|
||||
r1 = a1.r
|
||||
xc1, yc1 = a1.c.x, a1.c.y
|
||||
|
||||
r2 = a2.r
|
||||
xc2, yc2 = a2.c.x, a2.c.y
|
||||
|
||||
# construct tangency points for a related point-circle problem
|
||||
if r1 > r2:
|
||||
arc_tmp = Arc(a1.c, r1 - r2, a1.a1, a1.a2)
|
||||
xtmp1, ytmp1, xtmp2, ytmp2 = _pt_arc(a2.c, arc_tmp)
|
||||
|
||||
delta_r = r1 - r2
|
||||
|
||||
dx1 = (xtmp1 - xc1) / delta_r
|
||||
dy1 = (ytmp1 - yc1) / delta_r
|
||||
|
||||
dx2 = (xtmp2 - xc1) / delta_r
|
||||
dy2 = (ytmp2 - yc1) / delta_r
|
||||
|
||||
elif r1 < r2:
|
||||
arc_tmp = Arc(a2.c, r2 - r1, a2.a1, a2.a2)
|
||||
xtmp1, ytmp1, xtmp2, ytmp2 = _pt_arc(a1.c, arc_tmp)
|
||||
|
||||
delta_r = r2 - r1
|
||||
|
||||
dx1 = (xtmp1 - xc2) / delta_r
|
||||
dy1 = (ytmp1 - yc2) / delta_r
|
||||
|
||||
dx2 = (xtmp2 - xc2) / delta_r
|
||||
dy2 = (ytmp2 - yc2) / delta_r
|
||||
|
||||
else:
|
||||
dx = xc2 - xc1
|
||||
dy = yc2 - yc1
|
||||
l = sqrt(dx ** 2 + dy ** 2)
|
||||
|
||||
dx /= l
|
||||
dy /= l
|
||||
|
||||
dx1 = -dy
|
||||
dy1 = dx
|
||||
dx2 = dy
|
||||
dy2 = -dx
|
||||
|
||||
# construct the tangency points and angles
|
||||
x11 = xc1 + dx1 * r1
|
||||
y11 = yc1 + dy1 * r1
|
||||
x12 = xc1 + dx2 * r1
|
||||
y12 = yc1 + dy2 * r1
|
||||
|
||||
x21 = xc2 + dx1 * r2
|
||||
y21 = yc2 + dy1 * r2
|
||||
x22 = xc2 + dx2 * r2
|
||||
y22 = yc2 + dy2 * r2
|
||||
|
||||
a1_out = atan2p(x21 - x11, y21 - y11)
|
||||
a2_out = atan2p(x22 - x12, y22 - y12)
|
||||
|
||||
# select the feasible angle
|
||||
a11 = (atan2p(x11 - xc1, y11 - yc1) + pi / 2) % (2 * pi)
|
||||
a21 = (atan2p(x12 - xc1, y12 - yc1) + pi / 2) % (2 * pi)
|
||||
|
||||
ix = int(argmin((abs(a11 - a1_out), abs(a21 - a2_out))))
|
||||
angles = (a1_out, a2_out)
|
||||
segments = (
|
||||
Segment(Point(x11, y11), Point(x21, y21)),
|
||||
Segment(Point(x12, y12), Point(x22, y22)),
|
||||
)
|
||||
|
||||
return angles[ix], segments[ix]
|
||||
|
||||
|
||||
def get_angle(current: Entity, e: Entity) -> Tuple[float, Segment]:
|
||||
|
||||
if current is e:
|
||||
return inf, Segment(Point(inf, inf), Point(inf, inf))
|
||||
|
||||
if isinstance(current, Point):
|
||||
if isinstance(e, Point):
|
||||
return pt_pt(current, e)
|
||||
else:
|
||||
return pt_arc(current, e)
|
||||
else:
|
||||
if isinstance(e, Point):
|
||||
return arc_pt(current, e)
|
||||
else:
|
||||
return arc_arc(current, e)
|
||||
|
||||
|
||||
def update_hull(
|
||||
current_e: Entity,
|
||||
ix: int,
|
||||
entities: List[Entity],
|
||||
angles: List[float],
|
||||
segments: List[Segment],
|
||||
hull: Hull,
|
||||
) -> Tuple[Entity, float, bool]:
|
||||
|
||||
next_e = entities[ix]
|
||||
connecting_seg = segments[ix]
|
||||
|
||||
if isinstance(next_e, Point):
|
||||
entities.pop(ix)
|
||||
|
||||
hull.extend((connecting_seg, next_e))
|
||||
|
||||
return next_e, angles[ix], next_e is hull[0]
|
||||
|
||||
|
||||
def finalize_hull(hull: Hull) -> Wire:
|
||||
|
||||
rv = []
|
||||
|
||||
for el_p, el, el_n in zip(hull, hull[1:], hull[2:]):
|
||||
|
||||
if isinstance(el, Segment):
|
||||
rv.append(Edge.makeLine(Vector(el.a.x, el.a.y), Vector(el.b.x, el.b.y)))
|
||||
elif (
|
||||
isinstance(el, Arc)
|
||||
and isinstance(el_p, Segment)
|
||||
and isinstance(el_n, Segment)
|
||||
):
|
||||
a1 = degrees(atan2p(el_p.b.x - el.c.x, el_p.b.y - el.c.y))
|
||||
a2 = degrees(atan2p(el_n.a.x - el.c.x, el_n.a.y - el.c.y))
|
||||
|
||||
rv.append(
|
||||
Edge.makeCircle(el.r, Vector(el.c.x, el.c.y), angle1=a1, angle2=a2)
|
||||
)
|
||||
|
||||
el1 = hull[1]
|
||||
if isinstance(el, Segment) and isinstance(el_n, Arc) and isinstance(el1, Segment):
|
||||
a1 = degrees(atan2p(el.b.x - el_n.c.x, el.b.y - el_n.c.y))
|
||||
a2 = degrees(atan2p(el1.a.x - el_n.c.x, el1.a.y - el_n.c.y))
|
||||
|
||||
rv.append(
|
||||
Edge.makeCircle(el_n.r, Vector(el_n.c.x, el_n.c.y), angle1=a1, angle2=a2)
|
||||
)
|
||||
|
||||
return Wire.assembleEdges(rv)
|
||||
|
||||
|
||||
def find_hull(edges: Iterable[Edge]) -> Wire:
|
||||
|
||||
# initialize the hull
|
||||
rv: Hull = []
|
||||
|
||||
# split into arcs and points
|
||||
arcs, points = convert_and_validate(edges)
|
||||
|
||||
# select the starting element
|
||||
start = select_lowest(arcs, points)
|
||||
rv.append(start)
|
||||
|
||||
# initialize
|
||||
entities: List[Entity] = []
|
||||
entities.extend(arcs)
|
||||
entities.extend(points)
|
||||
|
||||
current_e = start
|
||||
current_angle = 0.0
|
||||
finished = False
|
||||
|
||||
# march around
|
||||
while not finished:
|
||||
|
||||
angles = []
|
||||
segments = []
|
||||
|
||||
for e in entities:
|
||||
angle, segment = get_angle(current_e, e)
|
||||
angles.append(angle if angle >= current_angle else inf)
|
||||
segments.append(segment)
|
||||
|
||||
next_ix = int(argmin(angles))
|
||||
current_e, current_angle, finished = update_hull(
|
||||
current_e, next_ix, entities, angles, segments, rv
|
||||
)
|
||||
|
||||
# convert back to Edges and return
|
||||
return finalize_hull(rv)
|
||||
@ -175,6 +175,9 @@ class Vector(object):
|
||||
def getAngle(self, v: "Vector") -> float:
|
||||
return self.wrapped.Angle(v.wrapped)
|
||||
|
||||
def getSignedAngle(self, v: "Vector") -> float:
|
||||
return self.wrapped.AngleWithRef(v.wrapped, gp_Vec(0, 0, -1))
|
||||
|
||||
def distanceToLine(self):
|
||||
raise NotImplementedError("Have not needed this yet, but OCCT supports it!")
|
||||
|
||||
|
||||
81
cadquery/occ_impl/importers/__init__.py
Normal file
81
cadquery/occ_impl/importers/__init__.py
Normal file
@ -0,0 +1,81 @@
|
||||
from math import pi
|
||||
|
||||
from ... import cq
|
||||
from ..shapes import Shape
|
||||
from .dxf import _importDXF
|
||||
|
||||
|
||||
from OCP.STEPControl import STEPControl_Reader
|
||||
|
||||
import OCP.IFSelect
|
||||
|
||||
RAD2DEG = 360.0 / (2 * pi)
|
||||
|
||||
|
||||
class ImportTypes:
|
||||
STEP = "STEP"
|
||||
DXF = "DXF"
|
||||
|
||||
|
||||
class UNITS:
|
||||
MM = "mm"
|
||||
IN = "in"
|
||||
|
||||
|
||||
def importShape(importType, fileName, *args, **kwargs):
|
||||
"""
|
||||
Imports a file based on the type (STEP, STL, etc)
|
||||
|
||||
:param importType: The type of file that we're importing
|
||||
:param fileName: THe name of the file that we're importing
|
||||
"""
|
||||
|
||||
# Check to see what type of file we're working with
|
||||
if importType == ImportTypes.STEP:
|
||||
return importStep(fileName, *args, **kwargs)
|
||||
elif importType == ImportTypes.DXF:
|
||||
return importDXF(fileName, *args, **kwargs)
|
||||
else:
|
||||
raise RuntimeError("Unsupported import type: {!r}".format(importType))
|
||||
|
||||
|
||||
# Loads a STEP file into a CQ.Workplane object
|
||||
def importStep(fileName):
|
||||
"""
|
||||
Accepts a file name and loads the STEP file into a cadquery Workplane
|
||||
|
||||
:param fileName: The path and name of the STEP file to be imported
|
||||
"""
|
||||
|
||||
# Now read and return the shape
|
||||
reader = STEPControl_Reader()
|
||||
readStatus = reader.ReadFile(fileName)
|
||||
if readStatus != OCP.IFSelect.IFSelect_RetDone:
|
||||
raise ValueError("STEP File could not be loaded")
|
||||
for i in range(reader.NbRootsForTransfer()):
|
||||
reader.TransferRoot(i + 1)
|
||||
|
||||
occ_shapes = []
|
||||
for i in range(reader.NbShapes()):
|
||||
occ_shapes.append(reader.Shape(i + 1))
|
||||
|
||||
# Make sure that we extract all the solids
|
||||
solids = []
|
||||
for shape in occ_shapes:
|
||||
solids.append(Shape.cast(shape))
|
||||
|
||||
return cq.Workplane("XY").newObject(solids)
|
||||
|
||||
|
||||
def importDXF(filename, tol=1e-6, exclude=[]):
|
||||
"""
|
||||
Loads a DXF file into a cadquery Workplane.
|
||||
|
||||
:param fileName: The path and name of the DXF file to be imported
|
||||
:param tol: The tolerance used for merging edges into wires (default: 1e-6)
|
||||
:param exclude: a list of layer names not to import (default: [])
|
||||
"""
|
||||
|
||||
faces = _importDXF(filename, tol, exclude)
|
||||
|
||||
return cq.Workplane("XY").newObject(faces)
|
||||
@ -1,13 +1,13 @@
|
||||
from collections import OrderedDict
|
||||
from math import pi
|
||||
from typing import List
|
||||
|
||||
from .. import cq
|
||||
from .geom import Vector
|
||||
from .shapes import Shape, Edge, Face, sortWiresByBuildOrder, DEG2RAD
|
||||
from ... import cq
|
||||
from ..geom import Vector
|
||||
from ..shapes import Shape, Edge, Face, sortWiresByBuildOrder
|
||||
|
||||
import ezdxf
|
||||
|
||||
from OCP.STEPControl import STEPControl_Reader
|
||||
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
|
||||
from OCP.TopTools import TopTools_HSequenceOfShape
|
||||
from OCP.gp import gp_Pnt
|
||||
@ -17,66 +17,9 @@ from OCP.TColStd import TColStd_Array1OfReal, TColStd_Array1OfInteger
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
|
||||
|
||||
|
||||
import OCP.IFSelect
|
||||
|
||||
RAD2DEG = 360.0 / (2 * pi)
|
||||
|
||||
|
||||
class ImportTypes:
|
||||
STEP = "STEP"
|
||||
DXF = "DXF"
|
||||
|
||||
|
||||
class UNITS:
|
||||
MM = "mm"
|
||||
IN = "in"
|
||||
|
||||
|
||||
def importShape(importType, fileName, *args, **kwargs):
|
||||
"""
|
||||
Imports a file based on the type (STEP, STL, etc)
|
||||
|
||||
:param importType: The type of file that we're importing
|
||||
:param fileName: THe name of the file that we're importing
|
||||
"""
|
||||
|
||||
# Check to see what type of file we're working with
|
||||
if importType == ImportTypes.STEP:
|
||||
return importStep(fileName, *args, **kwargs)
|
||||
elif importType == ImportTypes.DXF:
|
||||
return importDXF(fileName, *args, **kwargs)
|
||||
else:
|
||||
raise RuntimeError("Unsupported import type: {!r}".format(importType))
|
||||
|
||||
|
||||
# Loads a STEP file into a CQ.Workplane object
|
||||
def importStep(fileName):
|
||||
"""
|
||||
Accepts a file name and loads the STEP file into a cadquery Workplane
|
||||
|
||||
:param fileName: The path and name of the STEP file to be imported
|
||||
"""
|
||||
|
||||
# Now read and return the shape
|
||||
reader = STEPControl_Reader()
|
||||
readStatus = reader.ReadFile(fileName)
|
||||
if readStatus != OCP.IFSelect.IFSelect_RetDone:
|
||||
raise ValueError("STEP File could not be loaded")
|
||||
for i in range(reader.NbRootsForTransfer()):
|
||||
reader.TransferRoot(i + 1)
|
||||
|
||||
occ_shapes = []
|
||||
for i in range(reader.NbShapes()):
|
||||
occ_shapes.append(reader.Shape(i + 1))
|
||||
|
||||
# Make sure that we extract all the solids
|
||||
solids = []
|
||||
for shape in occ_shapes:
|
||||
solids.append(Shape.cast(shape))
|
||||
|
||||
return cq.Workplane("XY").newObject(solids)
|
||||
|
||||
|
||||
def _dxf_line(el):
|
||||
|
||||
try:
|
||||
@ -140,7 +83,7 @@ def _dxf_spline(el):
|
||||
if el.weights:
|
||||
rational = True
|
||||
|
||||
weights = OCP.TColStd.TColStd_Array1OfReal(1, len(el.weights))
|
||||
weights = TColStd_Array1OfReal(1, len(el.weights))
|
||||
for i, w in enumerate(el.weights):
|
||||
weights.SetValue(i + 1, w)
|
||||
|
||||
@ -214,9 +157,9 @@ def _dxf_convert(elements, tol):
|
||||
return rv
|
||||
|
||||
|
||||
def importDXF(filename, tol=1e-6, exclude=[]):
|
||||
def _importDXF(filename: str, tol: float = 1e-6, exclude: List[str] = []) -> List[Face]:
|
||||
"""
|
||||
Loads a DXF file into a cadquery Workplane.
|
||||
Loads a DXF file into a list of faces.
|
||||
|
||||
:param fileName: The path and name of the DXF file to be imported
|
||||
:param tol: The tolerance used for merging edges into wires (default: 1e-6)
|
||||
@ -236,4 +179,4 @@ def importDXF(filename, tol=1e-6, exclude=[]):
|
||||
for wire_set in wire_sets:
|
||||
faces.append(Face.makeFromWires(wire_set[0], wire_set[1:]))
|
||||
|
||||
return cq.Workplane("XY").newObject(faces)
|
||||
return faces
|
||||
@ -17,6 +17,8 @@ from typing_extensions import Literal, Protocol
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from multimethod import multimethod
|
||||
|
||||
from vtkmodules.vtkCommonDataModel import vtkPolyData
|
||||
from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals
|
||||
|
||||
@ -231,6 +233,8 @@ from OCP.Standard import Standard_NoSuchObject, Standard_Failure
|
||||
from math import pi, sqrt
|
||||
import warnings
|
||||
|
||||
Real = Union[float, int]
|
||||
|
||||
TOLERANCE = 1e-6
|
||||
DEG2RAD = 2 * pi / 360.0
|
||||
HASH_CODE_MAX = 2147483647 # max 32bit signed int, required by OCC.Core.HashCode
|
||||
@ -372,6 +376,7 @@ class Shape(object):
|
||||
"""
|
||||
|
||||
wrapped: TopoDS_Shape
|
||||
forConstruction: bool
|
||||
|
||||
def __init__(self, obj: TopoDS_Shape):
|
||||
self.wrapped = downcast(obj)
|
||||
@ -399,9 +404,7 @@ class Shape(object):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def cast(
|
||||
cls: Type["Shape"], obj: TopoDS_Shape, forConstruction: bool = False
|
||||
) -> "Shape":
|
||||
def cast(cls, obj: TopoDS_Shape, forConstruction: bool = False) -> "Shape":
|
||||
"Returns the right type of wrapper, given a OCCT object"
|
||||
|
||||
tr = None
|
||||
@ -871,12 +874,12 @@ class Shape(object):
|
||||
|
||||
return self._apply_transform(T)
|
||||
|
||||
def copy(self) -> "Shape":
|
||||
def copy(self: T) -> T:
|
||||
"""
|
||||
Creates a new object that is a copy of this object.
|
||||
"""
|
||||
|
||||
return Shape.cast(BRepBuilderAPI_Copy(self.wrapped).Shape())
|
||||
return self.__class__(BRepBuilderAPI_Copy(self.wrapped).Shape())
|
||||
|
||||
def transformShape(self, tMatrix: Matrix) -> "Shape":
|
||||
"""
|
||||
@ -933,12 +936,12 @@ class Shape(object):
|
||||
|
||||
return self
|
||||
|
||||
def located(self, loc: Location) -> "Shape":
|
||||
def located(self: T, loc: Location) -> T:
|
||||
"""
|
||||
Apply a location in absolute sense to a copy of self
|
||||
"""
|
||||
|
||||
r = Shape.cast(self.wrapped.Located(loc.wrapped))
|
||||
r = self.__class__(self.wrapped.Located(loc.wrapped))
|
||||
r.forConstruction = self.forConstruction
|
||||
|
||||
return r
|
||||
@ -952,12 +955,12 @@ class Shape(object):
|
||||
|
||||
return self
|
||||
|
||||
def moved(self, loc: Location) -> "Shape":
|
||||
def moved(self: T, loc: Location) -> T:
|
||||
"""
|
||||
Apply a location in relative sense (i.e. update current location) to a copy of self
|
||||
"""
|
||||
|
||||
r = Shape.cast(self.wrapped.Moved(loc.wrapped))
|
||||
r = self.__class__(self.wrapped.Moved(loc.wrapped))
|
||||
r.forConstruction = self.forConstruction
|
||||
|
||||
return r
|
||||
@ -1266,7 +1269,7 @@ class Vertex(Shape):
|
||||
return Vector(self.toTuple())
|
||||
|
||||
@classmethod
|
||||
def makeVertex(cls: Type["Vertex"], x: float, y: float, z: float) -> "Vertex":
|
||||
def makeVertex(cls, x: float, y: float, z: float) -> "Vertex":
|
||||
|
||||
return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)).Vertex())
|
||||
|
||||
@ -1296,11 +1299,17 @@ class Mixin1DProtocol(ShapeProtocol, Protocol):
|
||||
d: float,
|
||||
mode: Literal["length", "parameter"] = "length",
|
||||
frame: Literal["frenet", "corrected"] = "frenet",
|
||||
planar: bool = False,
|
||||
) -> Location:
|
||||
...
|
||||
|
||||
|
||||
class Mixin1D(object):
|
||||
def _bounds(self: Mixin1DProtocol) -> Tuple[float, float]:
|
||||
|
||||
curve = self._geomAdaptor()
|
||||
return curve.FirstParameter(), curve.LastParameter()
|
||||
|
||||
def startPoint(self: Mixin1DProtocol) -> Vector:
|
||||
"""
|
||||
|
||||
@ -1464,11 +1473,13 @@ class Mixin1D(object):
|
||||
d: float,
|
||||
mode: Literal["length", "parameter"] = "length",
|
||||
frame: Literal["frenet", "corrected"] = "frenet",
|
||||
planar: bool = False,
|
||||
) -> Location:
|
||||
"""Generate a location along the underlying curve.
|
||||
:param d: distance or parameter value
|
||||
:param mode: position calculation mode (default: length)
|
||||
:param frame: moving frame calculation method (default: frenet)
|
||||
:param planar: planar mode
|
||||
:return: A Location object representing local coordinate system at the specified distance.
|
||||
"""
|
||||
|
||||
@ -1493,9 +1504,14 @@ class Mixin1D(object):
|
||||
pnt = curve.Value(param)
|
||||
|
||||
T = gp_Trsf()
|
||||
T.SetTransformation(
|
||||
gp_Ax3(pnt, gp_Dir(tangent.XYZ()), gp_Dir(normal.XYZ())), gp_Ax3()
|
||||
)
|
||||
if planar:
|
||||
T.SetTransformation(
|
||||
gp_Ax3(pnt, gp_Dir(0, 0, 1), gp_Dir(normal.XYZ())), gp_Ax3()
|
||||
)
|
||||
else:
|
||||
T.SetTransformation(
|
||||
gp_Ax3(pnt, gp_Dir(tangent.XYZ()), gp_Dir(normal.XYZ())), gp_Ax3()
|
||||
)
|
||||
|
||||
return Location(TopLoc_Location(T))
|
||||
|
||||
@ -1504,15 +1520,17 @@ class Mixin1D(object):
|
||||
ds: Iterable[float],
|
||||
mode: Literal["length", "parameter"] = "length",
|
||||
frame: Literal["frenet", "corrected"] = "frenet",
|
||||
planar: bool = False,
|
||||
) -> List[Location]:
|
||||
"""Generate location along the curve
|
||||
:param ds: distance or parameter values
|
||||
:param mode: position calculation mode (default: length)
|
||||
:param frame: moving frame calculation method (default: frenet)
|
||||
:param planar: planar mode
|
||||
:return: A list of Location objects representing local coordinate systems at the specified distances.
|
||||
"""
|
||||
|
||||
return [self.locationAt(d, mode, frame) for d in ds]
|
||||
return [self.locationAt(d, mode, frame, planar) for d in ds]
|
||||
|
||||
|
||||
class Edge(Shape, Mixin1D):
|
||||
@ -1551,14 +1569,32 @@ class Edge(Shape, Mixin1D):
|
||||
|
||||
return rv
|
||||
|
||||
def arcCenter(self) -> Vector:
|
||||
"""
|
||||
Center of an underlying circle or ellipse geometry.
|
||||
"""
|
||||
|
||||
g = self.geomType()
|
||||
a = self._geomAdaptor()
|
||||
|
||||
if g == "CIRCLE":
|
||||
rv = Vector(a.Circle().Position().Location())
|
||||
elif g == "ELLIPSE":
|
||||
rv = Vector(a.Ellipse().Position().Location())
|
||||
else:
|
||||
raise ValueError(f"{g} has no arc center")
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def makeCircle(
|
||||
cls: Type["Edge"],
|
||||
cls,
|
||||
radius: float,
|
||||
pnt: VectorLike = Vector(0, 0, 0),
|
||||
dir: VectorLike = Vector(0, 0, 1),
|
||||
angle1: float = 360.0,
|
||||
angle2: float = 360,
|
||||
orientation=True,
|
||||
) -> "Edge":
|
||||
pnt = Vector(pnt)
|
||||
dir = Vector(dir)
|
||||
@ -1569,13 +1605,13 @@ class Edge(Shape, Mixin1D):
|
||||
return cls(BRepBuilderAPI_MakeEdge(circle_gp).Edge())
|
||||
else: # arc case
|
||||
circle_geom = GC_MakeArcOfCircle(
|
||||
circle_gp, angle1 * DEG2RAD, angle2 * DEG2RAD, True
|
||||
circle_gp, angle1 * DEG2RAD, angle2 * DEG2RAD, orientation
|
||||
).Value()
|
||||
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
|
||||
|
||||
@classmethod
|
||||
def makeEllipse(
|
||||
cls: Type["Edge"],
|
||||
cls,
|
||||
x_radius: float,
|
||||
y_radius: float,
|
||||
pnt: VectorLike = Vector(0, 0, 0),
|
||||
@ -1632,7 +1668,7 @@ class Edge(Shape, Mixin1D):
|
||||
|
||||
@classmethod
|
||||
def makeSpline(
|
||||
cls: Type["Edge"],
|
||||
cls,
|
||||
listOfVector: List[Vector],
|
||||
tangents: Optional[Sequence[Vector]] = None,
|
||||
periodic: bool = False,
|
||||
@ -1711,7 +1747,7 @@ class Edge(Shape, Mixin1D):
|
||||
|
||||
@classmethod
|
||||
def makeSplineApprox(
|
||||
cls: Type["Edge"],
|
||||
cls,
|
||||
listOfVector: List[Vector],
|
||||
tol: float = 1e-3,
|
||||
smoothing: Optional[Tuple[float, float, float]] = None,
|
||||
@ -1749,9 +1785,7 @@ class Edge(Shape, Mixin1D):
|
||||
return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge())
|
||||
|
||||
@classmethod
|
||||
def makeThreePointArc(
|
||||
cls: Type["Edge"], v1: Vector, v2: Vector, v3: Vector
|
||||
) -> "Edge":
|
||||
def makeThreePointArc(cls, v1: Vector, v2: Vector, v3: Vector) -> "Edge":
|
||||
"""
|
||||
Makes a three point arc through the provided points
|
||||
:param cls:
|
||||
@ -1765,7 +1799,7 @@ class Edge(Shape, Mixin1D):
|
||||
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
|
||||
|
||||
@classmethod
|
||||
def makeTangentArc(cls: Type["Edge"], v1: Vector, v2: Vector, v3: Vector) -> "Edge":
|
||||
def makeTangentArc(cls, v1: Vector, v2: Vector, v3: Vector) -> "Edge":
|
||||
"""
|
||||
Makes a tangent arc from point v1, in the direction of v2 and ends at
|
||||
v3.
|
||||
@ -1780,7 +1814,7 @@ class Edge(Shape, Mixin1D):
|
||||
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
|
||||
|
||||
@classmethod
|
||||
def makeLine(cls: Type["Edge"], v1: Vector, v2: Vector) -> "Edge":
|
||||
def makeLine(cls, v1: Vector, v2: Vector) -> "Edge":
|
||||
"""
|
||||
Create a line between two points
|
||||
:param v1: Vector that represents the first point
|
||||
@ -1828,7 +1862,7 @@ class Wire(Shape, Mixin1D):
|
||||
|
||||
@classmethod
|
||||
def combine(
|
||||
cls: Type["Wire"], listOfWires: Iterable[Union["Wire", Edge]], tol: float = 1e-9
|
||||
cls, listOfWires: Iterable[Union["Wire", Edge]], tol: float = 1e-9
|
||||
) -> List["Wire"]:
|
||||
"""
|
||||
Attempt to combine a list of wires and edges into a new wire.
|
||||
@ -1849,7 +1883,7 @@ class Wire(Shape, Mixin1D):
|
||||
return [cls(el) for el in wires_out]
|
||||
|
||||
@classmethod
|
||||
def assembleEdges(cls: Type["Wire"], listOfEdges: Iterable[Edge]) -> "Wire":
|
||||
def assembleEdges(cls, listOfEdges: Iterable[Edge]) -> "Wire":
|
||||
"""
|
||||
Attempts to build a wire that consists of the edges in the provided list
|
||||
:param cls:
|
||||
@ -1878,9 +1912,7 @@ class Wire(Shape, Mixin1D):
|
||||
return cls(wire_builder.Wire())
|
||||
|
||||
@classmethod
|
||||
def makeCircle(
|
||||
cls: Type["Wire"], radius: float, center: Vector, normal: Vector
|
||||
) -> "Wire":
|
||||
def makeCircle(cls, radius: float, center: Vector, normal: Vector) -> "Wire":
|
||||
"""
|
||||
Makes a Circle centered at the provided point, having normal in the provided direction
|
||||
:param radius: floating point radius of the circle, must be > 0
|
||||
@ -1895,7 +1927,7 @@ class Wire(Shape, Mixin1D):
|
||||
|
||||
@classmethod
|
||||
def makeEllipse(
|
||||
cls: Type["Wire"],
|
||||
cls,
|
||||
x_radius: float,
|
||||
y_radius: float,
|
||||
center: Vector,
|
||||
@ -1935,9 +1967,7 @@ class Wire(Shape, Mixin1D):
|
||||
|
||||
@classmethod
|
||||
def makePolygon(
|
||||
cls: Type["Wire"],
|
||||
listOfVertices: Iterable[Vector],
|
||||
forConstruction: bool = False,
|
||||
cls, listOfVertices: Iterable[Vector], forConstruction: bool = False,
|
||||
) -> "Wire":
|
||||
# convert list of tuples into Vectors.
|
||||
wire_builder = BRepBuilderAPI_MakePolygon()
|
||||
@ -1952,7 +1982,7 @@ class Wire(Shape, Mixin1D):
|
||||
|
||||
@classmethod
|
||||
def makeHelix(
|
||||
cls: Type["Wire"],
|
||||
cls,
|
||||
pitch: float,
|
||||
height: float,
|
||||
radius: float,
|
||||
@ -2114,7 +2144,7 @@ class Face(Shape):
|
||||
|
||||
@classmethod
|
||||
def makeNSidedSurface(
|
||||
cls: Type["Face"],
|
||||
cls,
|
||||
edges: Iterable[Edge],
|
||||
points: Iterable[gp_Pnt],
|
||||
continuity: GeomAbs_Shape = GeomAbs_C0,
|
||||
@ -2181,7 +2211,7 @@ class Face(Shape):
|
||||
|
||||
@classmethod
|
||||
def makePlane(
|
||||
cls: Type["Face"],
|
||||
cls,
|
||||
length: Optional[float] = None,
|
||||
width: Optional[float] = None,
|
||||
basePnt: VectorLike = (0, 0, 0),
|
||||
@ -2203,16 +2233,12 @@ class Face(Shape):
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def makeRuledSurface(
|
||||
cls: Type["Face"], edgeOrWire1: Edge, edgeOrWire2: Edge
|
||||
) -> "Face":
|
||||
def makeRuledSurface(cls, edgeOrWire1: Edge, edgeOrWire2: Edge) -> "Face":
|
||||
...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def makeRuledSurface(
|
||||
cls: Type["Face"], edgeOrWire1: Wire, edgeOrWire2: Wire
|
||||
) -> "Face":
|
||||
def makeRuledSurface(cls, edgeOrWire1: Wire, edgeOrWire2: Wire) -> "Face":
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@ -2229,9 +2255,7 @@ class Face(Shape):
|
||||
return cls.cast(BRepFill.Face_s(edgeOrWire1.wrapped, edgeOrWire2.wrapped))
|
||||
|
||||
@classmethod
|
||||
def makeFromWires(
|
||||
cls: Type["Face"], outerWire: Wire, innerWires: List[Wire] = []
|
||||
) -> "Face":
|
||||
def makeFromWires(cls, outerWire: Wire, innerWires: List[Wire] = []) -> "Face":
|
||||
"""
|
||||
Makes a planar face from one or more wires
|
||||
"""
|
||||
@ -2266,7 +2290,7 @@ class Face(Shape):
|
||||
|
||||
@classmethod
|
||||
def makeSplineApprox(
|
||||
cls: Type["Face"],
|
||||
cls,
|
||||
points: List[List[Vector]],
|
||||
tol: float = 1e-2,
|
||||
smoothing: Optional[Tuple[float, float, float]] = None,
|
||||
@ -2362,7 +2386,7 @@ class Shell(Shape):
|
||||
wrapped: TopoDS_Shell
|
||||
|
||||
@classmethod
|
||||
def makeShell(cls: Type["Shell"], listOfFaces: Iterable[Face]) -> "Shell":
|
||||
def makeShell(cls, listOfFaces: Iterable[Face]) -> "Shell":
|
||||
|
||||
shell_builder = BRepBuilderAPI_Sewing()
|
||||
|
||||
@ -2507,32 +2531,47 @@ class Mixin3D(object):
|
||||
|
||||
return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace()
|
||||
|
||||
@multimethod
|
||||
def dprism(
|
||||
self: TS,
|
||||
basis: Optional[Face],
|
||||
profiles: List[Wire],
|
||||
depth: Optional[float] = None,
|
||||
taper: float = 0,
|
||||
depth: Optional[Real] = None,
|
||||
taper: Real = 0,
|
||||
upToFace: Optional[Face] = None,
|
||||
thruAll: bool = True,
|
||||
additive: bool = True,
|
||||
) -> TS:
|
||||
) -> "Solid":
|
||||
"""
|
||||
Make a prismatic feature (additive or subtractive)
|
||||
|
||||
:param basis: face to perfrom the operation on
|
||||
:param basis: face to perform the operation on
|
||||
:param profiles: list of profiles
|
||||
:param depth: depth of the cut or extrusion
|
||||
:param upToFace: a face to extrude until
|
||||
:param thruAll: cut thruAll
|
||||
:param additive: set the kind of operation (additive or subtractive)
|
||||
:return: a Solid object
|
||||
"""
|
||||
|
||||
sorted_profiles = sortWiresByBuildOrder(profiles)
|
||||
faces = [Face.makeFromWires(p[0], p[1:]) for p in sorted_profiles]
|
||||
|
||||
return self.dprism(basis, faces, depth, taper, upToFace, thruAll, additive)
|
||||
|
||||
@dprism.register
|
||||
def dprism(
|
||||
self: TS,
|
||||
basis: Optional[Face],
|
||||
faces: List[Face],
|
||||
depth: Optional[Real] = None,
|
||||
taper: Real = 0,
|
||||
upToFace: Optional[Face] = None,
|
||||
thruAll: bool = True,
|
||||
additive: bool = True,
|
||||
) -> "Solid":
|
||||
|
||||
shape: Union[TopoDS_Shape, TopoDS_Solid] = self.wrapped
|
||||
for p in sorted_profiles:
|
||||
face = Face.makeFromWires(p[0], p[1:])
|
||||
for face in faces:
|
||||
feat = BRepFeat_MakeDPrism(
|
||||
shape,
|
||||
face.wrapped,
|
||||
@ -2541,6 +2580,7 @@ class Mixin3D(object):
|
||||
additive,
|
||||
False,
|
||||
)
|
||||
|
||||
if upToFace is not None:
|
||||
feat.Perform(upToFace.wrapped)
|
||||
elif thruAll or depth is None:
|
||||
@ -2562,7 +2602,7 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def interpPlate(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
surf_edges,
|
||||
surf_pts,
|
||||
thickness,
|
||||
@ -2681,13 +2721,13 @@ class Solid(Shape, Mixin3D):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def makeSolid(cls: Type["Solid"], shell: Shell) -> "Solid":
|
||||
def makeSolid(cls, shell: Shell) -> "Solid":
|
||||
|
||||
return cls(ShapeFix_Solid().SolidFromShell(shell.wrapped))
|
||||
|
||||
@classmethod
|
||||
def makeBox(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
length: float,
|
||||
width: float,
|
||||
height: float,
|
||||
@ -2706,7 +2746,7 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def makeCone(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
radius1: float,
|
||||
radius2: float,
|
||||
height: float,
|
||||
@ -2731,7 +2771,7 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def makeCylinder(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
radius: float,
|
||||
height: float,
|
||||
pnt: Vector = Vector(0, 0, 0),
|
||||
@ -2751,7 +2791,7 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def makeTorus(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
radius1: float,
|
||||
radius2: float,
|
||||
pnt: Vector = Vector(0, 0, 0),
|
||||
@ -2776,9 +2816,7 @@ class Solid(Shape, Mixin3D):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def makeLoft(
|
||||
cls: Type["Solid"], listOfWire: List[Wire], ruled: bool = False
|
||||
) -> "Solid":
|
||||
def makeLoft(cls, listOfWire: List[Wire], ruled: bool = False) -> "Solid":
|
||||
"""
|
||||
makes a loft from a list of wires
|
||||
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
|
||||
@ -2798,7 +2836,7 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def makeWedge(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
dx: float,
|
||||
dy: float,
|
||||
dz: float,
|
||||
@ -2822,7 +2860,7 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def makeSphere(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
radius: float,
|
||||
pnt: Vector = Vector(0, 0, 0),
|
||||
dir: Vector = Vector(0, 0, 1),
|
||||
@ -2846,7 +2884,7 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def _extrudeAuxSpine(
|
||||
cls: Type["Solid"], wire: TopoDS_Wire, spine: TopoDS_Wire, auxSpine: TopoDS_Wire
|
||||
cls, wire: TopoDS_Wire, spine: TopoDS_Wire, auxSpine: TopoDS_Wire
|
||||
) -> TopoDS_Shape:
|
||||
"""
|
||||
Helper function for extrudeLinearWithRotation
|
||||
@ -2858,14 +2896,14 @@ class Solid(Shape, Mixin3D):
|
||||
extrude_builder.MakeSolid()
|
||||
return extrude_builder.Shape()
|
||||
|
||||
@classmethod
|
||||
@multimethod
|
||||
def extrudeLinearWithRotation(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
outerWire: Wire,
|
||||
innerWires: List[Wire],
|
||||
vecCenter: Vector,
|
||||
vecNormal: Vector,
|
||||
angleDegrees: float,
|
||||
angleDegrees: Real,
|
||||
) -> "Solid":
|
||||
"""
|
||||
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
|
||||
@ -2916,12 +2954,22 @@ class Solid(Shape, Mixin3D):
|
||||
return cls(BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape())
|
||||
|
||||
@classmethod
|
||||
@extrudeLinearWithRotation.register
|
||||
def extrudeLinearWithRotation(
|
||||
cls, face: Face, vecCenter: Vector, vecNormal: Vector, angleDegrees: Real,
|
||||
) -> "Solid":
|
||||
|
||||
return cls.extrudeLinearWithRotation(
|
||||
face.outerWire(), face.innerWires(), vecCenter, vecNormal, angleDegrees
|
||||
)
|
||||
|
||||
@multimethod
|
||||
def extrudeLinear(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
outerWire: Wire,
|
||||
innerWires: List[Wire],
|
||||
vecNormal: Vector,
|
||||
taper: float = 0,
|
||||
taper: Real = 0,
|
||||
) -> "Solid":
|
||||
"""
|
||||
Attempt to extrude the list of wires into a prismatic solid in the provided direction
|
||||
@ -2948,11 +2996,20 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
if taper == 0:
|
||||
face = Face.makeFromWires(outerWire, innerWires)
|
||||
else:
|
||||
face = Face.makeFromWires(outerWire)
|
||||
|
||||
return cls.extrudeLinear(face, vecNormal, taper)
|
||||
|
||||
@classmethod
|
||||
@extrudeLinear.register
|
||||
def extrudeLinear(cls, face: Face, vecNormal: Vector, taper: Real = 0,) -> "Solid":
|
||||
|
||||
if taper == 0:
|
||||
prism_builder: Any = BRepPrimAPI_MakePrism(
|
||||
face.wrapped, vecNormal.wrapped, True
|
||||
)
|
||||
else:
|
||||
face = Face.makeFromWires(outerWire)
|
||||
faceNormal = face.normalAt()
|
||||
d = 1 if vecNormal.getAngle(faceNormal) < 90 * DEG2RAD else -1
|
||||
prism_builder = LocOpe_DPrism(
|
||||
@ -2961,12 +3018,12 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
return cls(prism_builder.Shape())
|
||||
|
||||
@classmethod
|
||||
@multimethod
|
||||
def revolve(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
outerWire: Wire,
|
||||
innerWires: List[Wire],
|
||||
angleDegrees: float,
|
||||
angleDegrees: Real,
|
||||
axisStart: Vector,
|
||||
axisEnd: Vector,
|
||||
) -> "Solid":
|
||||
@ -2996,6 +3053,14 @@ class Solid(Shape, Mixin3D):
|
||||
"""
|
||||
face = Face.makeFromWires(outerWire, innerWires)
|
||||
|
||||
return cls.revolve(face, angleDegrees, axisStart, axisEnd)
|
||||
|
||||
@classmethod
|
||||
@revolve.register
|
||||
def revolve(
|
||||
cls, face: Face, angleDegrees: Real, axisStart: Vector, axisEnd: Vector,
|
||||
) -> "Solid":
|
||||
|
||||
v1 = Vector(axisStart)
|
||||
v2 = Vector(axisEnd)
|
||||
v2 = v2 - v1
|
||||
@ -3042,9 +3107,9 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
@multimethod
|
||||
def sweep(
|
||||
cls: Type["Solid"],
|
||||
cls,
|
||||
outerWire: Wire,
|
||||
innerWires: List[Wire],
|
||||
path: Union[Wire, Edge],
|
||||
@ -3099,10 +3164,32 @@ class Solid(Shape, Mixin3D):
|
||||
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
@sweep.register
|
||||
def sweep(
|
||||
cls,
|
||||
face: Face,
|
||||
path: Union[Wire, Edge],
|
||||
makeSolid: bool = True,
|
||||
isFrenet: bool = False,
|
||||
mode: Union[Vector, Wire, Edge, None] = None,
|
||||
transitionMode: Literal["transformed", "round", "right"] = "transformed",
|
||||
) -> "Shape":
|
||||
|
||||
return cls.sweep(
|
||||
face.outerWire(),
|
||||
face.innerWires(),
|
||||
path,
|
||||
makeSolid,
|
||||
isFrenet,
|
||||
mode,
|
||||
transitionMode,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def sweep_multi(
|
||||
cls: Type["Solid"],
|
||||
profiles: List[Wire],
|
||||
cls,
|
||||
profiles: Iterable[Union[Wire, Face]],
|
||||
path: Union[Wire, Edge],
|
||||
makeSolid: bool = True,
|
||||
isFrenet: bool = False,
|
||||
@ -3132,7 +3219,8 @@ class Solid(Shape, Mixin3D):
|
||||
builder.SetMode(isFrenet)
|
||||
|
||||
for p in profiles:
|
||||
builder.Add(p.wrapped, translate, rotate)
|
||||
w = p.wrapped if isinstance(p, Wire) else p.outerWire().wrapped
|
||||
builder.Add(w, translate, rotate)
|
||||
|
||||
builder.Build()
|
||||
|
||||
@ -3169,10 +3257,16 @@ class Compound(Shape, Mixin3D):
|
||||
|
||||
return comp
|
||||
|
||||
def remove(self, shape: Shape):
|
||||
"""
|
||||
Remove the specified shape.
|
||||
"""
|
||||
|
||||
comp_builder = TopoDS_Builder()
|
||||
comp_builder.Remove(self.wrapped, shape.wrapped)
|
||||
|
||||
@classmethod
|
||||
def makeCompound(
|
||||
cls: Type["Compound"], listOfShapes: Iterable[Shape]
|
||||
) -> "Compound":
|
||||
def makeCompound(cls, listOfShapes: Iterable[Shape]) -> "Compound":
|
||||
"""
|
||||
Create a compound out of a list of shapes
|
||||
"""
|
||||
@ -3181,7 +3275,7 @@ class Compound(Shape, Mixin3D):
|
||||
|
||||
@classmethod
|
||||
def makeText(
|
||||
cls: Type["Compound"],
|
||||
cls,
|
||||
text: str,
|
||||
size: float,
|
||||
height: float,
|
||||
@ -3254,18 +3348,25 @@ class Compound(Shape, Mixin3D):
|
||||
yield Shape.cast(it.Value())
|
||||
it.Next()
|
||||
|
||||
def cut(self, *toCut: Shape) -> "Shape":
|
||||
def __bool__(self) -> bool:
|
||||
"""
|
||||
Check if empty.
|
||||
"""
|
||||
|
||||
return TopoDS_Iterator(self.wrapped).More()
|
||||
|
||||
def cut(self, *toCut: Shape) -> "Compound":
|
||||
"""
|
||||
Remove a shape from another one
|
||||
"""
|
||||
|
||||
cut_op = BRepAlgoAPI_Cut()
|
||||
|
||||
return self._bool_op(self, toCut, cut_op)
|
||||
return tcast(Compound, self._bool_op(self, toCut, cut_op))
|
||||
|
||||
def fuse(
|
||||
self, *toFuse: Shape, glue: bool = False, tol: Optional[float] = None
|
||||
) -> "Shape":
|
||||
) -> "Compound":
|
||||
"""
|
||||
Fuse shapes together
|
||||
"""
|
||||
@ -3279,23 +3380,23 @@ class Compound(Shape, Mixin3D):
|
||||
args = tuple(self) + toFuse
|
||||
|
||||
if len(args) <= 1:
|
||||
rv: Shape = self
|
||||
rv: Shape = args[0]
|
||||
else:
|
||||
rv = self._bool_op(args[:1], args[1:], fuse_op)
|
||||
|
||||
# fuse_op.RefineEdges()
|
||||
# fuse_op.FuseEdges()
|
||||
|
||||
return rv
|
||||
return tcast(Compound, rv)
|
||||
|
||||
def intersect(self, *toIntersect: Shape) -> "Shape":
|
||||
def intersect(self, *toIntersect: Shape) -> "Compound":
|
||||
"""
|
||||
Construct shape intersection
|
||||
"""
|
||||
|
||||
intersect_op = BRepAlgoAPI_Common()
|
||||
|
||||
return self._bool_op(self, toIntersect, intersect_op)
|
||||
return tcast(Compound, self._bool_op(self, toIntersect, intersect_op))
|
||||
|
||||
|
||||
def sortWiresByBuildOrder(wireList: List[Wire]) -> List[List[Wire]]:
|
||||
@ -3329,3 +3430,26 @@ def sortWiresByBuildOrder(wireList: List[Wire]) -> List[List[Wire]]:
|
||||
rv.append([face.outerWire(),] + face.innerWires())
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def wiresToFaces(wireList: List[Wire]) -> List[Face]:
|
||||
"""
|
||||
Convert wires to a list of faces.
|
||||
"""
|
||||
|
||||
return Face.makeFromWires(wireList[0], wireList[1:]).Faces()
|
||||
|
||||
|
||||
def edgesToWires(edges: Iterable[Edge], tol: float = 1e-6) -> List[Wire]:
|
||||
"""
|
||||
Convert edges to a list of wires.
|
||||
"""
|
||||
|
||||
edges_in = TopTools_HSequenceOfShape()
|
||||
wires_out = TopTools_HSequenceOfShape()
|
||||
|
||||
for e in edges:
|
||||
edges_in.Append(e.wrapped)
|
||||
ShapeAnalysis_FreeBounds.ConnectEdgesToWires_s(edges_in, tol, False, wires_out)
|
||||
|
||||
return [Wire(el) for el in wires_out]
|
||||
|
||||
378
cadquery/occ_impl/sketch_solver.py
Normal file
378
cadquery/occ_impl/sketch_solver.py
Normal file
@ -0,0 +1,378 @@
|
||||
from typing import Tuple, Union, Any, Callable, List, Optional, Iterable, Dict, Sequence
|
||||
from typing_extensions import Literal
|
||||
from nptyping import NDArray as Array
|
||||
from itertools import accumulate, chain
|
||||
from math import sin, cos, radians
|
||||
|
||||
from numpy import array, full, inf, sign
|
||||
from numpy.linalg import norm
|
||||
import nlopt
|
||||
|
||||
from OCP.gp import gp_Vec2d
|
||||
|
||||
from .shapes import Geoms
|
||||
from ..types import Real
|
||||
|
||||
NoneType = type(None)
|
||||
|
||||
SegmentDOF = Tuple[float, float, float, float] # p1 p2
|
||||
ArcDOF = Tuple[float, float, float, float, float] # p r a da
|
||||
DOF = Union[SegmentDOF, ArcDOF]
|
||||
|
||||
ConstraintKind = Literal[
|
||||
"Fixed",
|
||||
"FixedPoint",
|
||||
"Coincident",
|
||||
"Angle",
|
||||
"Length",
|
||||
"Distance",
|
||||
"Radius",
|
||||
"Orientation",
|
||||
"ArcAngle",
|
||||
]
|
||||
|
||||
ConstraintInvariants = { # (arity, geometry types, param type, conversion func)
|
||||
"Fixed": (1, ("CIRCLE", "LINE"), NoneType, None),
|
||||
"FixedPoint": (1, ("CIRCLE", "LINE"), Optional[Real], None),
|
||||
"Coincident": (2, ("CIRCLE", "LINE"), NoneType, None),
|
||||
"Angle": (2, ("CIRCLE", "LINE"), Real, radians),
|
||||
"Length": (1, ("CIRCLE", "LINE"), Real, None),
|
||||
"Distance": (
|
||||
2,
|
||||
("CIRCLE", "LINE"),
|
||||
Tuple[Optional[Real], Optional[Real], Real],
|
||||
None,
|
||||
),
|
||||
"Radius": (1, ("CIRCLE",), Real, None),
|
||||
"Orientation": (1, ("LINE",), Tuple[Real, Real], None),
|
||||
"ArcAngle": (1, ("CIRCLE",), Real, radians),
|
||||
}
|
||||
|
||||
Constraint = Tuple[Tuple[int, Optional[int]], ConstraintKind, Optional[Any]]
|
||||
|
||||
DIFF_EPS = 1e-10
|
||||
TOL = 1e-9
|
||||
MAXITER = 0
|
||||
|
||||
|
||||
def invalid_args(*t):
|
||||
|
||||
return ValueError("Invalid argument types {t}")
|
||||
|
||||
|
||||
def arc_first(x):
|
||||
|
||||
return array((x[0] + x[2] * sin(x[3]), x[1] + x[2] * cos(x[3])))
|
||||
|
||||
|
||||
def arc_last(x):
|
||||
|
||||
return array((x[0] + x[2] * sin(x[3] + x[4]), x[1] + x[2] * cos(x[3] + x[4])))
|
||||
|
||||
|
||||
def arc_point(x, val):
|
||||
|
||||
if val is None:
|
||||
rv = x[:2]
|
||||
else:
|
||||
a = x[3] + val * x[4]
|
||||
rv = array((x[0] + x[2] * sin(a), x[1] + x[2] * cos(a)))
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def line_point(x, val):
|
||||
|
||||
return x[:2] + val * x[2:]
|
||||
|
||||
|
||||
def arc_first_tangent(x):
|
||||
|
||||
return gp_Vec2d(sign(x[4]) * cos(x[3]), -sign(x[4]) * sin(x[3]))
|
||||
|
||||
|
||||
def arc_last_tangent(x):
|
||||
|
||||
return gp_Vec2d(sign(x[4]) * cos(x[3] + x[4]), -sign(x[4]) * sin(x[3] + x[4]))
|
||||
|
||||
|
||||
def fixed_cost(x, t, x0, val):
|
||||
|
||||
return norm(x - x0)
|
||||
|
||||
|
||||
def fixed_point_cost(x, t, x0, val):
|
||||
|
||||
if t == "LINE":
|
||||
rv = norm(line_point(x, val) - line_point(x0, val))
|
||||
elif t == "CIRCLE":
|
||||
rv = norm(arc_point(x, val) - arc_point(x0, val))
|
||||
else:
|
||||
raise invalid_args(t)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def coincident_cost(x1, t1, x10, x2, t2, x20, val):
|
||||
|
||||
if t1 == "LINE" and t2 == "LINE":
|
||||
v1 = x1[2:]
|
||||
v2 = x2[:2]
|
||||
elif t1 == "LINE" and t2 == "CIRCLE":
|
||||
v1 = x1[2:]
|
||||
v2 = arc_first(x2)
|
||||
elif t1 == "CIRCLE" and t2 == "LINE":
|
||||
v1 = arc_last(x1)
|
||||
v2 = x2[:2]
|
||||
elif t1 == "CIRCLE" and t2 == "CIRCLE":
|
||||
v1 = arc_last(x1)
|
||||
v2 = arc_first(x2)
|
||||
else:
|
||||
raise invalid_args(t1, t2)
|
||||
|
||||
return norm(v1 - v2)
|
||||
|
||||
|
||||
def angle_cost(x1, t1, x10, x2, t2, x20, val):
|
||||
|
||||
if t1 == "LINE" and t2 == "LINE":
|
||||
v1 = gp_Vec2d(*(x1[2:] - x1[:2]))
|
||||
v2 = gp_Vec2d(*(x2[2:] - x2[:2]))
|
||||
elif t1 == "LINE" and t2 == "CIRCLE":
|
||||
v1 = gp_Vec2d(*(x1[2:] - x1[:2]))
|
||||
v2 = arc_first_tangent(x2)
|
||||
elif t1 == "CIRCLE" and t2 == "LINE":
|
||||
v1 = arc_last_tangent(x1)
|
||||
v2 = gp_Vec2d(*(x2[2:] - x2[:2]))
|
||||
elif t1 == "CIRCLE" and t2 == "CIRCLE":
|
||||
v1 = arc_last_tangent(x1)
|
||||
v2 = arc_first_tangent(x2)
|
||||
else:
|
||||
raise invalid_args(t1, t2)
|
||||
|
||||
return v2.Angle(v1) - val
|
||||
|
||||
|
||||
def length_cost(x, t, x0, val):
|
||||
|
||||
rv = 0
|
||||
|
||||
if t == "LINE":
|
||||
rv = norm(x[2:] - x[:2]) - val
|
||||
elif t == "CIRCLE":
|
||||
rv = norm(x[2] * (x[4] - x[3])) - val
|
||||
else:
|
||||
raise invalid_args(t)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def distance_cost(x1, t1, x10, x2, t2, x20, val):
|
||||
|
||||
val1, val2, d = val
|
||||
|
||||
if t1 == "LINE" and t2 == "LINE":
|
||||
v1 = line_point(x1, val1)
|
||||
v2 = line_point(x2, val2)
|
||||
elif t1 == "LINE" and t2 == "CIRCLE":
|
||||
v1 = line_point(x1, val1)
|
||||
v2 = arc_point(x2, val2)
|
||||
elif t1 == "CIRCLE" and t2 == "LINE":
|
||||
v1 = arc_point(x1, val1)
|
||||
v2 = line_point(x2, val2)
|
||||
elif t1 == "CIRCLE" and t2 == "CIRCLE":
|
||||
v1 = arc_point(x1, val1)
|
||||
v2 = arc_point(x2, val2)
|
||||
else:
|
||||
raise invalid_args(t1, t2)
|
||||
|
||||
return norm(v1 - v2) - d
|
||||
|
||||
|
||||
def radius_cost(x, t, x0, val):
|
||||
|
||||
if t == "CIRCLE":
|
||||
rv = x[2] - val
|
||||
else:
|
||||
raise invalid_args(t)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def orientation_cost(x, t, x0, val):
|
||||
|
||||
if t == "LINE":
|
||||
rv = gp_Vec2d(*(x[2:] - x[:2])).Angle(gp_Vec2d(*val))
|
||||
else:
|
||||
raise invalid_args(t)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def arc_angle_cost(x, t, x0, val):
|
||||
|
||||
if t == "CIRCLE":
|
||||
rv = norm(x[4] - x[3]) - val
|
||||
else:
|
||||
raise invalid_args(t)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
# dictionary of individual constraint cost functions
|
||||
costs: Dict[str, Callable[..., float]] = dict(
|
||||
Fixed=fixed_cost,
|
||||
FixedPoint=fixed_point_cost,
|
||||
Coincident=coincident_cost,
|
||||
Angle=angle_cost,
|
||||
Length=length_cost,
|
||||
Distance=distance_cost,
|
||||
Radius=radius_cost,
|
||||
Orientation=orientation_cost,
|
||||
ArcAngle=arc_angle_cost,
|
||||
)
|
||||
|
||||
|
||||
class SketchConstraintSolver(object):
|
||||
|
||||
entities: List[DOF]
|
||||
constraints: List[Constraint]
|
||||
geoms: List[Geoms]
|
||||
ne: int
|
||||
nc: int
|
||||
ixs: List[int]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entities: Iterable[DOF],
|
||||
constraints: Iterable[Constraint],
|
||||
geoms: Iterable[Geoms],
|
||||
):
|
||||
|
||||
self.entities = list(entities)
|
||||
self.constraints = list(constraints)
|
||||
self.geoms = list(geoms)
|
||||
|
||||
self.ne = len(self.entities)
|
||||
self.nc = len(self.constraints)
|
||||
|
||||
# validate and transform constraints
|
||||
|
||||
# indices of x corresponding to the entities
|
||||
self.ixs = [0] + list(accumulate(len(e) for e in self.entities))
|
||||
|
||||
def _cost(
|
||||
self, x0: Array[(Any,), float]
|
||||
) -> Tuple[
|
||||
Callable[[Array[(Any,), float]], float],
|
||||
Callable[[Array[(Any,), float], Array[(Any,), float]], None],
|
||||
Array[(Any,), float],
|
||||
Array[(Any,), float],
|
||||
]:
|
||||
|
||||
ixs = self.ixs
|
||||
constraints = self.constraints
|
||||
geoms = self.geoms
|
||||
|
||||
# split initial values per entity
|
||||
x0s = [x0[ixs[e] : ixs[e + 1]] for e in range(self.ne)]
|
||||
|
||||
def f(x) -> float:
|
||||
"""
|
||||
Cost function to be minimized
|
||||
"""
|
||||
|
||||
rv = 0.0
|
||||
|
||||
for i, ((e1, e2), kind, val) in enumerate(constraints):
|
||||
|
||||
cost = costs[kind]
|
||||
|
||||
# build arguments for the specific constraint
|
||||
args = [x[ixs[e1] : ixs[e1 + 1]], geoms[e1], x0s[e1]]
|
||||
if e2 is not None:
|
||||
args += [x[ixs[e2] : ixs[e2 + 1]], geoms[e2], x0s[e2]]
|
||||
|
||||
# evaluate
|
||||
rv += cost(*args, val) ** 2
|
||||
|
||||
return rv
|
||||
|
||||
def grad(x, rv) -> None:
|
||||
"""
|
||||
Gradient of the cost function
|
||||
"""
|
||||
|
||||
rv[:] = 0
|
||||
|
||||
for i, ((e1, e2), kind, val) in enumerate(constraints):
|
||||
|
||||
cost = costs[kind]
|
||||
|
||||
# build arguments for the specific constraint
|
||||
x1 = x[ixs[e1] : ixs[e1 + 1]]
|
||||
args = [x1.copy(), geoms[e1], x0s[e1]]
|
||||
if e2 is not None:
|
||||
x2 = x[ixs[e2] : ixs[e2 + 1]]
|
||||
args += [x2.copy(), geoms[e2], x0s[e2]]
|
||||
|
||||
# evaluate
|
||||
tmp = cost(*args, val)
|
||||
|
||||
for j, k in enumerate(range(ixs[e1], ixs[e1 + 1])):
|
||||
args[0][j] += DIFF_EPS
|
||||
tmp1 = cost(*args, val)
|
||||
rv[k] += 2 * tmp * (tmp1 - tmp) / DIFF_EPS
|
||||
args[0][j] = x1[j]
|
||||
|
||||
if e2 is not None:
|
||||
for j, k in enumerate(range(ixs[e2], ixs[e2 + 1])):
|
||||
args[3][j] += DIFF_EPS
|
||||
tmp2 = cost(*args, val)
|
||||
rv[k] += 2 * tmp * (tmp2 - tmp) / DIFF_EPS
|
||||
args[3][j] = x2[j]
|
||||
|
||||
# generate lower and upper bounds for optimization
|
||||
lb = full(ixs[-1], -inf)
|
||||
ub = full(ixs[-1], +inf)
|
||||
|
||||
for i, g in enumerate(geoms):
|
||||
if g == "CIRCLE":
|
||||
lb[ixs[i] + 2] = 0 # lower bound for radius
|
||||
|
||||
return f, grad, lb, ub
|
||||
|
||||
def solve(self) -> Tuple[Sequence[Sequence[float]], Dict[str, Any]]:
|
||||
|
||||
x0 = array(list(chain.from_iterable(self.entities))).ravel()
|
||||
f, grad, lb, ub = self._cost(x0)
|
||||
|
||||
def func(x, g):
|
||||
|
||||
if g.size > 0:
|
||||
grad(x, g)
|
||||
|
||||
return f(x)
|
||||
|
||||
opt = nlopt.opt(nlopt.LD_SLSQP, len(x0))
|
||||
opt.set_min_objective(func)
|
||||
opt.set_lower_bounds(lb)
|
||||
opt.set_upper_bounds(ub)
|
||||
|
||||
opt.set_ftol_abs(0)
|
||||
opt.set_ftol_rel(0)
|
||||
opt.set_xtol_rel(TOL)
|
||||
opt.set_xtol_abs(TOL * 1e-3)
|
||||
opt.set_maxeval(MAXITER)
|
||||
|
||||
x = opt.optimize(x0)
|
||||
status = {
|
||||
"entities": self.entities,
|
||||
"cost": opt.last_optimum_value(),
|
||||
"iters": opt.get_numevals(),
|
||||
"status": opt.last_optimize_result(),
|
||||
}
|
||||
|
||||
ixs = self.ixs
|
||||
|
||||
return [x[i1:i2] for i1, i2 in zip(ixs, ixs[1:])], status
|
||||
@ -89,7 +89,7 @@ class ConstraintSolver(object):
|
||||
self,
|
||||
) -> Tuple[
|
||||
Callable[[Array[(Any,), float]], float],
|
||||
Callable[[Array[(Any,), float]], Array[(Any,), float]],
|
||||
Callable[[Array[(Any,), float], Array[(Any,), float]], None],
|
||||
]:
|
||||
def pt_cost(
|
||||
m1: gp_Pnt,
|
||||
@ -160,14 +160,14 @@ class ConstraintSolver(object):
|
||||
|
||||
return rv
|
||||
|
||||
def jac(x):
|
||||
def grad(x, rv):
|
||||
|
||||
constraints = self.constraints
|
||||
ne = self.ne
|
||||
|
||||
delta = DIFF_EPS * eye(NDOF)
|
||||
|
||||
rv = zeros(NDOF * ne)
|
||||
rv[:] = 0
|
||||
|
||||
transforms = [
|
||||
self._build_transform(*x[NDOF * i : NDOF * (i + 1)]) for i in range(ne)
|
||||
@ -234,19 +234,17 @@ class ConstraintSolver(object):
|
||||
else:
|
||||
raise NotImplementedError(f"{m1,m2}")
|
||||
|
||||
return rv
|
||||
|
||||
return f, jac
|
||||
return f, grad
|
||||
|
||||
def solve(self) -> Tuple[List[Location], Dict[str, Any]]:
|
||||
|
||||
x0 = array([el for el in self.entities]).ravel()
|
||||
f, jac = self._cost()
|
||||
f, grad = self._cost()
|
||||
|
||||
def func(x, grad):
|
||||
def func(x, g):
|
||||
|
||||
if grad.size > 0:
|
||||
grad[:] = jac(x)
|
||||
if g.size > 0:
|
||||
grad(x, g)
|
||||
|
||||
return f(x)
|
||||
|
||||
|
||||
998
cadquery/sketch.py
Normal file
998
cadquery/sketch.py
Normal file
@ -0,0 +1,998 @@
|
||||
from typing import (
|
||||
Union,
|
||||
Optional,
|
||||
List,
|
||||
Dict,
|
||||
Callable,
|
||||
Tuple,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Any,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
cast as tcast,
|
||||
)
|
||||
from typing_extensions import Literal
|
||||
from math import tan, sin, cos, pi, radians
|
||||
from itertools import product, chain
|
||||
from multimethod import multimethod
|
||||
from typish import instance_of, get_type
|
||||
|
||||
from .hull import find_hull
|
||||
from .selectors import StringSyntaxSelector, Selector
|
||||
from .types import Real
|
||||
|
||||
from .occ_impl.shapes import Shape, Face, Edge, Wire, Compound, Vertex, edgesToWires
|
||||
from .occ_impl.geom import Location, Vector
|
||||
from .occ_impl.importers.dxf import _importDXF
|
||||
from .occ_impl.sketch_solver import (
|
||||
SketchConstraintSolver,
|
||||
ConstraintKind,
|
||||
ConstraintInvariants,
|
||||
DOF,
|
||||
arc_first,
|
||||
arc_last,
|
||||
arc_point,
|
||||
)
|
||||
|
||||
Modes = Literal["a", "s", "i"]
|
||||
Point = Union[Vector, Tuple[Real, Real]]
|
||||
|
||||
T = TypeVar("T", bound="Sketch")
|
||||
SketchVal = Union[Shape, Location]
|
||||
|
||||
|
||||
class Constraint(object):
|
||||
|
||||
tags: Tuple[str, ...]
|
||||
args: Tuple[Edge, ...]
|
||||
kind: ConstraintKind
|
||||
param: Any
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: Tuple[str, ...],
|
||||
args: Tuple[Edge, ...],
|
||||
kind: ConstraintKind,
|
||||
param: Any = None,
|
||||
):
|
||||
|
||||
# validate based on the solver provided spec
|
||||
if kind not in ConstraintInvariants:
|
||||
raise ValueError(f"Unknown constraint {kind}.")
|
||||
|
||||
arity, types, param_type, converter = ConstraintInvariants[kind]
|
||||
|
||||
if arity != len(tags):
|
||||
raise ValueError(
|
||||
f"Invalid number of entities for constraint {kind}. Provided {len(tags)}, required {arity}."
|
||||
)
|
||||
|
||||
if any(e.geomType() not in types for e in args):
|
||||
raise ValueError(
|
||||
f"Unsupported geometry types {[e.geomType() for e in args]} for constraint {kind}."
|
||||
)
|
||||
|
||||
if not instance_of(param, param_type):
|
||||
raise ValueError(
|
||||
f"Unsupported argument types {get_type(param)}, required {param_type}."
|
||||
)
|
||||
|
||||
# if all is fine store everything and possibly convert the params
|
||||
self.tags = tags
|
||||
self.args = args
|
||||
self.kind = kind
|
||||
self.param = tcast(Any, converter)(param) if converter else param
|
||||
|
||||
|
||||
class Sketch(object):
|
||||
"""
|
||||
2D sketch. Supports faces, edges and edges with constraints based construction.
|
||||
"""
|
||||
|
||||
parent: Any
|
||||
locs: List[Location]
|
||||
|
||||
_faces: Compound
|
||||
_wires: List[Wire]
|
||||
_edges: List[Edge]
|
||||
|
||||
_selection: List[SketchVal]
|
||||
_constraints: List[Constraint]
|
||||
|
||||
_tags: Dict[str, Sequence[SketchVal]]
|
||||
|
||||
_solve_status: Optional[Dict[str, Any]]
|
||||
|
||||
def __init__(self: T, parent: Any = None, locs: Iterable[Location] = (Location(),)):
|
||||
"""
|
||||
Construct an empty sketch.
|
||||
"""
|
||||
|
||||
self.parent = parent
|
||||
self.locs = list(locs)
|
||||
|
||||
self._faces = Compound.makeCompound(())
|
||||
self._wires = []
|
||||
self._edges = []
|
||||
|
||||
self._selection = []
|
||||
self._constraints = []
|
||||
|
||||
self._tags = {}
|
||||
|
||||
self._solve_status = None
|
||||
|
||||
def __iter__(self) -> Iterator[Face]:
|
||||
"""
|
||||
Iterate over faces-locations combinations.
|
||||
"""
|
||||
|
||||
return iter(f for l in self.locs for f in self._faces.moved(l).Faces())
|
||||
|
||||
def _tag(self: T, val: Sequence[Union[Shape, Location]], tag: str):
|
||||
|
||||
self._tags[tag] = val
|
||||
|
||||
# face construction
|
||||
def face(
|
||||
self: T,
|
||||
b: Union[Wire, Iterable[Edge], Compound, T],
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
ignore_selection: bool = False,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a face from a wire or edges.
|
||||
"""
|
||||
|
||||
res: Union[Face, Sketch, Compound]
|
||||
|
||||
if isinstance(b, Wire):
|
||||
res = Face.makeFromWires(b)
|
||||
elif isinstance(b, (Sketch, Compound)):
|
||||
res = b
|
||||
elif isinstance(b, Iterable):
|
||||
wires = edgesToWires(tcast(Iterable[Edge], b))
|
||||
res = Face.makeFromWires(*(wires[0], wires[1:]))
|
||||
else:
|
||||
raise ValueError(f"Unsupported argument {b}")
|
||||
|
||||
if angle != 0:
|
||||
res = res.moved(Location(Vector(), Vector(0, 0, 1), angle))
|
||||
|
||||
return self.each(lambda l: res.moved(l), mode, tag, ignore_selection)
|
||||
|
||||
def importDXF(
|
||||
self: T,
|
||||
filename: str,
|
||||
tol: float = 1e-6,
|
||||
exclude: List[str] = [],
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Import a DXF file and construct face(s)
|
||||
"""
|
||||
|
||||
res = Compound.makeCompound(_importDXF(filename, tol, exclude))
|
||||
|
||||
return self.face(res, angle, mode, tag)
|
||||
|
||||
def rect(
|
||||
self: T,
|
||||
w: Real,
|
||||
h: Real,
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a rectangular face.
|
||||
"""
|
||||
|
||||
res = Face.makePlane(w, h).rotate(Vector(), Vector(0, 0, 1), angle)
|
||||
|
||||
return self.each(lambda l: res.located(l), mode, tag)
|
||||
|
||||
def circle(self: T, r: Real, mode: Modes = "a", tag: Optional[str] = None) -> T:
|
||||
"""
|
||||
Construct a circular face.
|
||||
"""
|
||||
|
||||
res = Face.makeFromWires(Wire.makeCircle(r, Vector(), Vector(0, 0, 1)))
|
||||
|
||||
return self.each(lambda l: res.located(l), mode, tag)
|
||||
|
||||
def ellipse(
|
||||
self: T,
|
||||
a1: Real,
|
||||
a2: Real,
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Construct an elliptical face.
|
||||
"""
|
||||
|
||||
res = Face.makeFromWires(
|
||||
Wire.makeEllipse(
|
||||
a1, a2, Vector(), Vector(0, 0, 1), Vector(1, 0, 0), rotation_angle=angle
|
||||
)
|
||||
)
|
||||
|
||||
return self.each(lambda l: res.located(l), mode, tag)
|
||||
|
||||
def trapezoid(
|
||||
self: T,
|
||||
w: Real,
|
||||
h: Real,
|
||||
a1: Real,
|
||||
a2: Optional[float] = None,
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a trapezoidal face.
|
||||
"""
|
||||
|
||||
v1 = Vector(-w / 2, -h / 2)
|
||||
v2 = Vector(w / 2, -h / 2)
|
||||
v3 = Vector(-w / 2 + h / tan(radians(a1)), h / 2)
|
||||
v4 = Vector(w / 2 - h / tan(radians(a2) if a2 else radians(a1)), h / 2)
|
||||
|
||||
return self.polygon((v1, v2, v4, v3, v1), angle, mode, tag)
|
||||
|
||||
def slot(
|
||||
self: T,
|
||||
w: Real,
|
||||
h: Real,
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a slot-shaped face.
|
||||
"""
|
||||
|
||||
p1 = Vector(-w / 2, h / 2)
|
||||
p2 = Vector(w / 2, h / 2)
|
||||
p3 = Vector(-w / 2, -h / 2)
|
||||
p4 = Vector(w / 2, -h / 2)
|
||||
p5 = Vector(-w / 2 - h / 2, 0)
|
||||
p6 = Vector(w / 2 + h / 2, 0)
|
||||
|
||||
e1 = Edge.makeLine(p1, p2)
|
||||
e2 = Edge.makeThreePointArc(p2, p6, p4)
|
||||
e3 = Edge.makeLine(p4, p3)
|
||||
e4 = Edge.makeThreePointArc(p3, p5, p1)
|
||||
|
||||
wire = Wire.assembleEdges((e1, e2, e3, e4))
|
||||
|
||||
return self.face(wire, angle, mode, tag)
|
||||
|
||||
def regularPolygon(
|
||||
self: T,
|
||||
r: Real,
|
||||
n: int,
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a regular polygonal face.
|
||||
"""
|
||||
|
||||
pts = [
|
||||
Vector(r * sin(i * 2 * pi / n), r * cos(i * 2 * pi / n))
|
||||
for i in range(n + 1)
|
||||
]
|
||||
|
||||
return self.polygon(pts, angle, mode, tag)
|
||||
|
||||
def polygon(
|
||||
self: T,
|
||||
pts: Iterable[Point],
|
||||
angle: Real = 0,
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a polygonal face.
|
||||
"""
|
||||
|
||||
w = Wire.makePolygon(p if isinstance(p, Vector) else Vector(*p) for p in pts)
|
||||
|
||||
return self.face(w, angle, mode, tag)
|
||||
|
||||
# distribute locations
|
||||
|
||||
def rarray(self: T, xs: Real, ys: Real, nx: int, ny: int) -> T:
|
||||
"""
|
||||
Generate a rectangular array of locations.
|
||||
"""
|
||||
|
||||
if nx < 1 or ny < 1:
|
||||
raise ValueError(f"At least 1 elements required, requested {nx}, {ny}")
|
||||
|
||||
locs = []
|
||||
|
||||
offset = Vector((nx - 1) * xs, (ny - 1) * ys) * 0.5
|
||||
for i, j in product(range(nx), range(ny)):
|
||||
locs.append(Location(Vector(i * xs, j * ys) - offset))
|
||||
|
||||
if self._selection:
|
||||
selection: Sequence[Union[Shape, Location, Vector]] = self._selection
|
||||
else:
|
||||
selection = [Vector()]
|
||||
|
||||
return self.push(
|
||||
(el * l if isinstance(el, Location) else Location(el.Center())) * l
|
||||
for l in locs
|
||||
for el in selection
|
||||
)
|
||||
|
||||
def parray(self: T, r: Real, a1: Real, a2: Real, n: int, rotate: bool = True) -> T:
|
||||
"""
|
||||
Generate a polar array of locations.
|
||||
"""
|
||||
|
||||
if n < 1:
|
||||
raise ValueError(f"At least 1 elements required, requested {n}")
|
||||
|
||||
x = r * sin(radians(a1))
|
||||
y = r * cos(radians(a1))
|
||||
|
||||
if rotate:
|
||||
loc = Location(Vector(x, y), Vector(0, 0, 1), -a1)
|
||||
else:
|
||||
loc = Location(Vector(x, y))
|
||||
|
||||
locs = [loc]
|
||||
|
||||
angle = (a2 - a1) / (n - 1)
|
||||
|
||||
for i in range(1, n):
|
||||
phi = a1 + (angle * i)
|
||||
x = r * sin(radians(phi))
|
||||
y = r * cos(radians(phi))
|
||||
|
||||
if rotate:
|
||||
loc = Location(Vector(x, y), Vector(0, 0, 1), -phi)
|
||||
else:
|
||||
loc = Location(Vector(x, y))
|
||||
|
||||
locs.append(loc)
|
||||
|
||||
if self._selection:
|
||||
selection: Sequence[Union[Shape, Location, Vector]] = self._selection
|
||||
else:
|
||||
selection = [Vector()]
|
||||
|
||||
return self.push(
|
||||
(l * el if isinstance(el, Location) else l * Location(el.Center()))
|
||||
for l in locs
|
||||
for el in selection
|
||||
)
|
||||
|
||||
def distribute(
|
||||
self: T, n: int, start: Real = 0, stop: Real = 1, rotate: bool = True
|
||||
) -> T:
|
||||
"""
|
||||
Distribute locations along selected edges or wires.
|
||||
"""
|
||||
|
||||
if not self._selection:
|
||||
raise ValueError("Nothing selected to distirbute over")
|
||||
|
||||
params = [start + i * (stop - start) / n for i in range(n + 1)]
|
||||
|
||||
locs = []
|
||||
for el in self._selection:
|
||||
if isinstance(el, (Wire, Edge)):
|
||||
if rotate:
|
||||
locs.extend(el.locations(params, planar=True))
|
||||
else:
|
||||
locs.extend(Location(v) for v in el.positions(params))
|
||||
else:
|
||||
raise ValueError(f"Unsupported selection: {el}")
|
||||
|
||||
return self.push(locs)
|
||||
|
||||
def push(
|
||||
self: T, locs: Iterable[Union[Location, Point]], tag: Optional[str] = None,
|
||||
) -> T:
|
||||
"""
|
||||
Set current selection to given locations or points.
|
||||
"""
|
||||
|
||||
self._selection = [
|
||||
l if isinstance(l, Location) else Location(Vector(l)) for l in locs
|
||||
]
|
||||
|
||||
if tag:
|
||||
self._tag(self._selection[:], tag)
|
||||
|
||||
return self
|
||||
|
||||
def each(
|
||||
self: T,
|
||||
callback: Callable[[Location], Union[Face, "Sketch", Compound]],
|
||||
mode: Modes = "a",
|
||||
tag: Optional[str] = None,
|
||||
ignore_selection: bool = False,
|
||||
) -> T:
|
||||
"""
|
||||
Apply a callback on all applicable entities.
|
||||
"""
|
||||
|
||||
res: List[Face] = []
|
||||
locs: List[Location] = []
|
||||
|
||||
if self._selection and not ignore_selection:
|
||||
for el in self._selection:
|
||||
if isinstance(el, Location):
|
||||
loc = el
|
||||
else:
|
||||
loc = Location(el.Center())
|
||||
|
||||
locs.append(loc)
|
||||
|
||||
else:
|
||||
locs.append(Location())
|
||||
|
||||
for loc in locs:
|
||||
tmp = callback(loc)
|
||||
|
||||
if isinstance(tmp, Sketch):
|
||||
res.extend(tmp._faces.Faces())
|
||||
elif isinstance(tmp, Compound):
|
||||
res.extend(tmp.Faces())
|
||||
else:
|
||||
res.append(tmp)
|
||||
|
||||
if tag:
|
||||
self._tag(res, tag)
|
||||
|
||||
if mode == "a":
|
||||
self._faces = self._faces.fuse(*res)
|
||||
elif mode == "s":
|
||||
self._faces = self._faces.cut(*res)
|
||||
elif mode == "i":
|
||||
self._faces = self._faces.intersect(*res)
|
||||
elif mode == "c":
|
||||
if not tag:
|
||||
raise ValueError("No tag specified - the geometry will be unreachable")
|
||||
else:
|
||||
raise ValueError(f"Invalid mode: {mode}")
|
||||
|
||||
return self
|
||||
|
||||
# modifiers
|
||||
def hull(self: T, mode: Modes = "a", tag: Optional[str] = None) -> T:
|
||||
"""
|
||||
Generate a convex hull from current selection or all objects.
|
||||
"""
|
||||
|
||||
if self._selection:
|
||||
rv = find_hull(el for el in self._selection if isinstance(el, Edge))
|
||||
elif self._faces:
|
||||
rv = find_hull(el for el in self._faces.Edges())
|
||||
elif self._edges or self._wires:
|
||||
rv = find_hull(
|
||||
chain(self._edges, chain.from_iterable(w.Edges() for w in self._wires))
|
||||
)
|
||||
else:
|
||||
raise ValueError("No objects available for hull construction")
|
||||
|
||||
self.face(rv, mode=mode, tag=tag, ignore_selection=bool(self._selection))
|
||||
|
||||
return self
|
||||
|
||||
def offset(self: T, d: Real, mode: Modes = "a", tag: Optional[str] = None) -> T:
|
||||
"""
|
||||
Offset selected wires or edges.
|
||||
"""
|
||||
|
||||
rv = (el.offset2D(d) for el in self._selection if isinstance(el, Wire))
|
||||
|
||||
for el in chain.from_iterable(rv):
|
||||
self.face(el, mode=mode, tag=tag, ignore_selection=bool(self._selection))
|
||||
|
||||
return self
|
||||
|
||||
def _matchFacesToVertices(self) -> Dict[Face, List[Vertex]]:
|
||||
|
||||
rv = {}
|
||||
|
||||
for f in self._faces.Faces():
|
||||
|
||||
f_vertices = f.Vertices()
|
||||
rv[f] = [
|
||||
v for v in self._selection if isinstance(v, Vertex) and v in f_vertices
|
||||
]
|
||||
|
||||
return rv
|
||||
|
||||
def fillet(self: T, d: Real) -> T:
|
||||
"""
|
||||
Add a fillet based on current selection.
|
||||
"""
|
||||
|
||||
f2v = self._matchFacesToVertices()
|
||||
|
||||
self._faces = Compound.makeCompound(
|
||||
k.fillet2D(d, v) if v else k for k, v in f2v.items()
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def chamfer(self: T, d: Real) -> T:
|
||||
"""
|
||||
Add a chamfer based on current selection.
|
||||
"""
|
||||
|
||||
f2v = self._matchFacesToVertices()
|
||||
|
||||
self._faces = Compound.makeCompound(
|
||||
k.chamfer2D(d, v) if v else k for k, v in f2v.items()
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def clean(self: T) -> T:
|
||||
"""
|
||||
Remove internal wires.
|
||||
"""
|
||||
|
||||
self._faces = self._faces.clean()
|
||||
|
||||
return self
|
||||
|
||||
# selection
|
||||
|
||||
def _unique(self: T, vals: List[SketchVal]) -> List[SketchVal]:
|
||||
|
||||
tmp = {hash(v): v for v in vals}
|
||||
|
||||
return list(tmp.values())
|
||||
|
||||
def _select(
|
||||
self: T,
|
||||
s: Optional[Union[str, Selector]],
|
||||
kind: Literal["Faces", "Wires", "Edges", "Vertices"],
|
||||
tag: Optional[str] = None,
|
||||
) -> T:
|
||||
|
||||
rv = []
|
||||
|
||||
if tag:
|
||||
for el in self._tags[tag]:
|
||||
rv.extend(getattr(el, kind)())
|
||||
elif self._selection:
|
||||
for el in self._selection:
|
||||
if not isinstance(el, Location):
|
||||
rv.extend(getattr(el, kind)())
|
||||
else:
|
||||
rv.extend(getattr(self._faces, kind)())
|
||||
for el in self._edges:
|
||||
rv.extend(getattr(el, kind)())
|
||||
|
||||
if s and isinstance(s, Selector):
|
||||
filtered = s.filter(rv)
|
||||
elif s and isinstance(s, str):
|
||||
filtered = StringSyntaxSelector(s).filter(rv)
|
||||
else:
|
||||
filtered = rv
|
||||
|
||||
self._selection = self._unique(filtered)
|
||||
|
||||
return self
|
||||
|
||||
def tag(self: T, tag: str) -> T:
|
||||
"""
|
||||
Tag current selection.
|
||||
"""
|
||||
|
||||
self._tags[tag] = list(self._selection)
|
||||
|
||||
return self
|
||||
|
||||
def select(self: T, *tags: str) -> T:
|
||||
"""
|
||||
Select based on tags.
|
||||
"""
|
||||
|
||||
self._selection = []
|
||||
|
||||
for tag in tags:
|
||||
self._selection.extend(self._tags[tag])
|
||||
|
||||
return self
|
||||
|
||||
def faces(
|
||||
self: T, s: Optional[Union[str, Selector]] = None, tag: Optional[str] = None
|
||||
) -> T:
|
||||
"""
|
||||
Select faces.
|
||||
"""
|
||||
|
||||
return self._select(s, "Faces", tag)
|
||||
|
||||
def wires(
|
||||
self: T, s: Optional[Union[str, Selector]] = None, tag: Optional[str] = None
|
||||
) -> T:
|
||||
"""
|
||||
Select wires.
|
||||
"""
|
||||
|
||||
return self._select(s, "Wires", tag)
|
||||
|
||||
def edges(
|
||||
self: T, s: Optional[Union[str, Selector]] = None, tag: Optional[str] = None
|
||||
) -> T:
|
||||
"""
|
||||
Select edges.
|
||||
"""
|
||||
|
||||
return self._select(s, "Edges", tag)
|
||||
|
||||
def vertices(
|
||||
self: T, s: Optional[Union[str, Selector]] = None, tag: Optional[str] = None
|
||||
) -> T:
|
||||
"""
|
||||
Select vertices.
|
||||
"""
|
||||
|
||||
return self._select(s, "Vertices", tag)
|
||||
|
||||
def reset(self: T) -> T:
|
||||
"""
|
||||
Reset current selection.
|
||||
"""
|
||||
|
||||
self._selection = []
|
||||
return self
|
||||
|
||||
def delete(self: T) -> T:
|
||||
"""
|
||||
Delete selected object.
|
||||
"""
|
||||
|
||||
for obj in self._selection:
|
||||
if isinstance(obj, Face):
|
||||
self._faces.remove(obj)
|
||||
elif isinstance(obj, Wire):
|
||||
self._wires.remove(obj)
|
||||
elif isinstance(obj, Edge):
|
||||
self._edges.remove(obj)
|
||||
|
||||
self._selection = []
|
||||
|
||||
return self
|
||||
|
||||
# edge based interface
|
||||
|
||||
def _startPoint(self) -> Vector:
|
||||
|
||||
if not self._edges:
|
||||
raise ValueError("No free edges available")
|
||||
|
||||
e = self._edges[0]
|
||||
|
||||
return e.startPoint()
|
||||
|
||||
def _endPoint(self) -> Vector:
|
||||
|
||||
if not self._edges:
|
||||
raise ValueError("No free edges available")
|
||||
|
||||
e = self._edges[-1]
|
||||
|
||||
return e.endPoint()
|
||||
|
||||
def edge(
|
||||
self: T, val: Edge, tag: Optional[str] = None, forConstruction: bool = False
|
||||
) -> T:
|
||||
"""
|
||||
Add an edge to the sketch.
|
||||
"""
|
||||
|
||||
val.forConstruction = forConstruction
|
||||
self._edges.append(val)
|
||||
|
||||
if tag:
|
||||
self._tag([val], tag)
|
||||
|
||||
return self
|
||||
|
||||
@multimethod
|
||||
def segment(
|
||||
self: T,
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
tag: Optional[str] = None,
|
||||
forConstruction: bool = False,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a segment.
|
||||
"""
|
||||
|
||||
val = Edge.makeLine(Vector(p1), Vector(p2))
|
||||
|
||||
return self.edge(val, tag, forConstruction)
|
||||
|
||||
@segment.register
|
||||
def segment(
|
||||
self: T, p2: Point, tag: Optional[str] = None, forConstruction: bool = False
|
||||
) -> T:
|
||||
|
||||
p1 = self._endPoint()
|
||||
val = Edge.makeLine(p1, Vector(p2))
|
||||
|
||||
return self.edge(val, tag, forConstruction)
|
||||
|
||||
@segment.register
|
||||
def segment(
|
||||
self: T,
|
||||
l: Real,
|
||||
a: Real,
|
||||
tag: Optional[str] = None,
|
||||
forConstruction: bool = False,
|
||||
) -> T:
|
||||
|
||||
p1 = self._endPoint()
|
||||
d = Vector(l * cos(radians(a)), l * sin(radians(a)))
|
||||
val = Edge.makeLine(p1, p1 + d)
|
||||
|
||||
return self.edge(val, tag, forConstruction)
|
||||
|
||||
@multimethod
|
||||
def arc(
|
||||
self: T,
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
p3: Point,
|
||||
tag: Optional[str] = None,
|
||||
forConstruction: bool = False,
|
||||
) -> T:
|
||||
"""
|
||||
Construct an arc.
|
||||
"""
|
||||
|
||||
val = Edge.makeThreePointArc(Vector(p1), Vector(p2), Vector(p3))
|
||||
|
||||
return self.edge(val, tag, forConstruction)
|
||||
|
||||
@arc.register
|
||||
def arc(
|
||||
self: T,
|
||||
p2: Point,
|
||||
p3: Point,
|
||||
tag: Optional[str] = None,
|
||||
forConstruction: bool = False,
|
||||
) -> T:
|
||||
|
||||
p1 = self._endPoint()
|
||||
val = Edge.makeThreePointArc(Vector(p1), Vector(p2), Vector(p3))
|
||||
|
||||
return self.edge(val, tag, forConstruction)
|
||||
|
||||
@arc.register
|
||||
def arc(
|
||||
self: T,
|
||||
c: Point,
|
||||
r: Real,
|
||||
a: Real,
|
||||
da: Real,
|
||||
tag: Optional[str] = None,
|
||||
forConstruction: bool = False,
|
||||
) -> T:
|
||||
|
||||
if abs(da) >= 360:
|
||||
val = Edge.makeCircle(r, Vector(c), angle1=a, angle2=a, orientation=da > 0)
|
||||
else:
|
||||
p0 = Vector(c)
|
||||
p1 = p0 + r * Vector(cos(radians(a)), sin(radians(a)))
|
||||
p2 = p0 + r * Vector(cos(radians(a + da / 2)), sin(radians(a + da / 2)))
|
||||
p3 = p0 + r * Vector(cos(radians(a + da)), sin(radians(a + da)))
|
||||
val = Edge.makeThreePointArc(p1, p2, p3)
|
||||
|
||||
return self.edge(val, tag, forConstruction)
|
||||
|
||||
@multimethod
|
||||
def spline(
|
||||
self: T,
|
||||
pts: Iterable[Point],
|
||||
tangents: Optional[Iterable[Point]],
|
||||
periodic: bool,
|
||||
tag: Optional[str] = None,
|
||||
forConstruction: bool = False,
|
||||
) -> T:
|
||||
"""
|
||||
Construct a spline edge.
|
||||
"""
|
||||
|
||||
val = Edge.makeSpline(
|
||||
[Vector(*p) for p in pts],
|
||||
[Vector(*t) for t in tangents] if tangents else None,
|
||||
periodic,
|
||||
)
|
||||
|
||||
return self.edge(val, tag, forConstruction)
|
||||
|
||||
@spline.register
|
||||
def spline(
|
||||
self: T,
|
||||
pts: Iterable[Point],
|
||||
tag: Optional[str] = None,
|
||||
forConstruction: bool = False,
|
||||
) -> T:
|
||||
|
||||
return self.spline(pts, None, False, tag, forConstruction)
|
||||
|
||||
def close(self: T, tag: Optional[str] = None) -> T:
|
||||
"""
|
||||
Connect last edge to the first one.
|
||||
"""
|
||||
|
||||
self.segment(self._endPoint(), self._startPoint(), tag)
|
||||
|
||||
return self
|
||||
|
||||
def assemble(self: T, mode: Modes = "a", tag: Optional[str] = None) -> T:
|
||||
"""
|
||||
Assemble edges into faces.
|
||||
"""
|
||||
|
||||
return self.face(
|
||||
(e for e in self._edges if not e.forConstruction), 0, mode, tag
|
||||
)
|
||||
|
||||
# constraints
|
||||
@multimethod
|
||||
def constrain(self: T, tag: str, constraint: ConstraintKind, arg: Any) -> T:
|
||||
"""
|
||||
Add a constraint.
|
||||
"""
|
||||
|
||||
self._constraints.append(
|
||||
Constraint((tag,), (self._tags[tag][0],), constraint, arg)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
@constrain.register
|
||||
def constrain(
|
||||
self: T, tag1: str, tag2: str, constraint: ConstraintKind, arg: Any
|
||||
) -> T:
|
||||
|
||||
self._constraints.append(
|
||||
Constraint(
|
||||
(tag1, tag2),
|
||||
(self._tags[tag1][0], self._tags[tag2][0]),
|
||||
constraint,
|
||||
arg,
|
||||
)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def solve(self: T) -> T:
|
||||
"""
|
||||
Solve current constraints and update edge positions.
|
||||
"""
|
||||
|
||||
entities = [] # list with all degrees of freedom
|
||||
e2i = {} # mapping from tags to indices of entities
|
||||
geoms = [] # geometry types
|
||||
|
||||
# fill entities, e2i and geoms
|
||||
for i, (k, v) in enumerate(
|
||||
filter(lambda kv: isinstance(kv[1][0], Edge), self._tags.items())
|
||||
):
|
||||
|
||||
v0 = tcast(Edge, v[0])
|
||||
|
||||
# dispatch on geom type
|
||||
if v0.geomType() == "LINE":
|
||||
p1 = v0.startPoint()
|
||||
p2 = v0.endPoint()
|
||||
ent: DOF = (p1.x, p1.y, p2.x, p2.y)
|
||||
|
||||
elif v0.geomType() == "CIRCLE":
|
||||
p = v0.arcCenter()
|
||||
p1 = v0.startPoint() - p
|
||||
p2 = v0.endPoint() - p
|
||||
pm = v0.positionAt(0.5) - p
|
||||
|
||||
a1 = Vector(0, 1).getSignedAngle(p1)
|
||||
a2 = p1.getSignedAngle(p2)
|
||||
a3 = p1.getSignedAngle(pm)
|
||||
if a3 > 0 and a2 < 0:
|
||||
a2 += 2 * pi
|
||||
elif a3 < 0 and a2 > 0:
|
||||
a2 -= 2 * pi
|
||||
radius = v0.radius()
|
||||
ent = (p.x, p.y, radius, a1, a2)
|
||||
|
||||
else:
|
||||
continue
|
||||
|
||||
entities.append(ent)
|
||||
e2i[k] = i
|
||||
geoms.append(v0.geomType())
|
||||
|
||||
# build the POD constraint list
|
||||
constraints = []
|
||||
for c in self._constraints:
|
||||
ix = (e2i[c.tags[0]], e2i[c.tags[1]] if len(c.tags) == 2 else None)
|
||||
constraints.append((ix, c.kind, c.param))
|
||||
|
||||
# optimize
|
||||
solver = SketchConstraintSolver(entities, constraints, geoms)
|
||||
res, self._solve_status = solver.solve()
|
||||
self._solve_status["x"] = res
|
||||
|
||||
# translate back the solution - update edges
|
||||
for g, (k, i) in zip(geoms, e2i.items()):
|
||||
el = res[i]
|
||||
|
||||
# dispatch on geom type
|
||||
if g == "LINE":
|
||||
p1 = Vector(el[0], el[1])
|
||||
p2 = Vector(el[2], el[3])
|
||||
e = Edge.makeLine(p1, p2)
|
||||
elif g == "CIRCLE":
|
||||
p1 = Vector(*arc_first(el))
|
||||
p2 = Vector(*arc_point(el, 0.5))
|
||||
p3 = Vector(*arc_last(el))
|
||||
e = Edge.makeThreePointArc(p1, p2, p3)
|
||||
|
||||
# overwrite the low level object
|
||||
self._tags[k][0].wrapped = e.wrapped
|
||||
|
||||
return self
|
||||
|
||||
# misc
|
||||
|
||||
def copy(self: T) -> T:
|
||||
"""
|
||||
Create a partial copy of the sketch.
|
||||
"""
|
||||
|
||||
rv = self.__class__()
|
||||
rv._faces = self._faces.copy()
|
||||
|
||||
return rv
|
||||
|
||||
def moved(self: T, loc: Location) -> T:
|
||||
"""
|
||||
Create a partial copy of the sketch with moved _faces.
|
||||
"""
|
||||
|
||||
rv = self.__class__()
|
||||
rv._faces = self._faces.moved(loc)
|
||||
|
||||
return rv
|
||||
|
||||
def located(self: T, loc: Location) -> T:
|
||||
"""
|
||||
Create a partial copy of the sketch with a new location.
|
||||
"""
|
||||
|
||||
rv = self.__class__(locs=(loc,))
|
||||
rv._faces = self._faces.copy()
|
||||
|
||||
return rv
|
||||
|
||||
def finalize(self) -> Any:
|
||||
"""
|
||||
Finish sketch construction and return the parent
|
||||
"""
|
||||
|
||||
return self.parent
|
||||
3
cadquery/types.py
Normal file
3
cadquery/types.py
Normal file
@ -0,0 +1,3 @@
|
||||
from typing import Union
|
||||
|
||||
Real = Union[int, float]
|
||||
@ -23,6 +23,7 @@ requirements:
|
||||
- typing_extensions
|
||||
- nptyping
|
||||
- nlopt
|
||||
- multimethod 1.6
|
||||
|
||||
test:
|
||||
requires:
|
||||
|
||||
@ -4,10 +4,12 @@
|
||||
CadQuery API Reference
|
||||
***********************
|
||||
|
||||
The CadQuery API is made up of 2 main objects:
|
||||
The CadQuery API is made up of 4 main objects:
|
||||
|
||||
* **Sketch** -- Construct 2D sketches
|
||||
* **Workplane** -- Wraps a topological entity and provides a 2D modelling context.
|
||||
* **Selector** -- Filter and select things
|
||||
* **Assembly** -- Combine objects into assemblies.
|
||||
|
||||
This page lists methods of these objects grouped by **functional area**
|
||||
|
||||
@ -16,11 +18,88 @@ This page lists methods of these objects grouped by **functional area**
|
||||
Use :ref:`classreference` to see methods alphabetically by class.
|
||||
|
||||
|
||||
Initialization
|
||||
Sketch initialization
|
||||
---------------------
|
||||
|
||||
.. currentmodule:: cadquery
|
||||
|
||||
Creating new sketches.
|
||||
|
||||
.. autosummary::
|
||||
Sketch
|
||||
Sketch.importDXF
|
||||
Workplane.sketch
|
||||
Sketch.finalize
|
||||
Sketch.copy
|
||||
Sketch.located
|
||||
Sketch.moved
|
||||
|
||||
Sketch selection
|
||||
----------------
|
||||
|
||||
.. currentmodule:: cadquery
|
||||
|
||||
Selecting, tagging and manipulating elements.
|
||||
|
||||
.. autosummary::
|
||||
Sketch.tag
|
||||
Sketch.select
|
||||
Sketch.reset
|
||||
Sketch.delete
|
||||
Sketch.faces
|
||||
Sketch.edges
|
||||
Sketch.vertices
|
||||
|
||||
Sketching with faces
|
||||
--------------------
|
||||
|
||||
.. currentmodule:: cadquery
|
||||
|
||||
Sketching using the face-based API.
|
||||
|
||||
.. autosummary::
|
||||
Sketch.face
|
||||
Sketch.rect
|
||||
Sketch.circle
|
||||
Sketch.ellipse
|
||||
Sketch.trapezoid
|
||||
Sketch.slot
|
||||
Sketch.regularPolygon
|
||||
Sketch.polygon
|
||||
Sketch.rarray
|
||||
Sketch.parray
|
||||
Sketch.distribute
|
||||
Sketch.each
|
||||
Sketch.push
|
||||
Sketch.hull
|
||||
Sketch.offset
|
||||
Sketch.fillet
|
||||
Sketch.chamfer
|
||||
Sketch.clean
|
||||
|
||||
Sketching with edges and constraints
|
||||
------------------------------------
|
||||
|
||||
.. currentmodule:: cadquery
|
||||
|
||||
Sketching using the edge-based API.
|
||||
|
||||
.. autosummary::
|
||||
Sketch.edge
|
||||
Sketch.segment
|
||||
Sketch.arc
|
||||
Sketch.spline
|
||||
Sketch.close
|
||||
Sketch.assemble
|
||||
Sketch.constrain
|
||||
Sketch.solve
|
||||
|
||||
|
||||
Initialization
|
||||
--------------
|
||||
|
||||
.. currentmodule:: cadquery
|
||||
|
||||
Creating new workplanes and object chains
|
||||
|
||||
.. autosummary::
|
||||
@ -30,7 +109,7 @@ Creating new workplanes and object chains
|
||||
.. _2dOperations:
|
||||
|
||||
2D Operations
|
||||
-----------------
|
||||
-------------
|
||||
|
||||
Creating 2D constructs that can be used to create 3D features.
|
||||
|
||||
@ -173,25 +252,25 @@ as a basis for further operations.
|
||||
.. currentmodule:: cadquery.selectors
|
||||
.. autosummary::
|
||||
|
||||
NearestToPointSelector
|
||||
BoxSelector
|
||||
BaseDirSelector
|
||||
ParallelDirSelector
|
||||
DirectionSelector
|
||||
DirectionNthSelector
|
||||
LengthNthSelector
|
||||
AreaNthSelector
|
||||
RadiusNthSelector
|
||||
PerpendicularDirSelector
|
||||
TypeSelector
|
||||
DirectionMinMaxSelector
|
||||
CenterNthSelector
|
||||
BinarySelector
|
||||
AndSelector
|
||||
SumSelector
|
||||
SubtractSelector
|
||||
InverseSelector
|
||||
StringSyntaxSelector
|
||||
NearestToPointSelector
|
||||
BoxSelector
|
||||
BaseDirSelector
|
||||
ParallelDirSelector
|
||||
DirectionSelector
|
||||
DirectionNthSelector
|
||||
LengthNthSelector
|
||||
AreaNthSelector
|
||||
RadiusNthSelector
|
||||
PerpendicularDirSelector
|
||||
TypeSelector
|
||||
DirectionMinMaxSelector
|
||||
CenterNthSelector
|
||||
BinarySelector
|
||||
AndSelector
|
||||
SumSelector
|
||||
SubtractSelector
|
||||
InverseSelector
|
||||
StringSyntaxSelector
|
||||
|
||||
.. _assembly:
|
||||
|
||||
@ -203,10 +282,10 @@ Workplane and Shape objects can be connected together into assemblies
|
||||
.. currentmodule:: cadquery
|
||||
.. autosummary::
|
||||
|
||||
Assembly
|
||||
Assembly.add
|
||||
Assembly.save
|
||||
Assembly.constrain
|
||||
Assembly.solve
|
||||
Constraint
|
||||
Color
|
||||
Assembly
|
||||
Assembly.add
|
||||
Assembly.save
|
||||
Assembly.constrain
|
||||
Assembly.solve
|
||||
Constraint
|
||||
Color
|
||||
|
||||
@ -17,10 +17,10 @@ Core Classes
|
||||
|
||||
.. autosummary::
|
||||
|
||||
CQ
|
||||
Workplane
|
||||
Assembly
|
||||
Constraint
|
||||
Sketch
|
||||
Workplane
|
||||
Assembly
|
||||
Constraint
|
||||
|
||||
Topological Classes
|
||||
----------------------
|
||||
|
||||
@ -38,6 +38,7 @@ Table Of Contents
|
||||
quickstart.rst
|
||||
designprinciples.rst
|
||||
primer.rst
|
||||
sketch.rst
|
||||
assy.rst
|
||||
fileformat.rst
|
||||
examples.rst
|
||||
|
||||
272
doc/sketch.rst
Normal file
272
doc/sketch.rst
Normal file
@ -0,0 +1,272 @@
|
||||
.. _sketchtutorial:
|
||||
|
||||
******
|
||||
Sketch
|
||||
******
|
||||
|
||||
Sketch tutorial
|
||||
---------------
|
||||
|
||||
The purpose of this section is to demonstrate how to construct sketches using different
|
||||
approaches.
|
||||
|
||||
Face-based API
|
||||
==============
|
||||
|
||||
The main approach for constructing sketches is based on constructing faces and
|
||||
combining them using boolean operations.
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
result = (
|
||||
cq.Sketch()
|
||||
.trapezoid(4,3,90)
|
||||
.vertices()
|
||||
.circle(.5,mode='s')
|
||||
.reset()
|
||||
.vertices()
|
||||
.fillet(.25)
|
||||
.reset()
|
||||
.rarray(.6,1,5,1).slot(1.5,0.4,mode='s',angle=90)
|
||||
)
|
||||
|
||||
Note that selectors are implemented, but selection has to be explicitly reset. Sketch
|
||||
class does not implement history and all modifications happen in-place.
|
||||
|
||||
|
||||
Edge-based API
|
||||
==============
|
||||
|
||||
If needed, one can construct sketches by placing individual edges.
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
result = (
|
||||
cq.Sketch()
|
||||
.segment((0.,0),(0.,2.))
|
||||
.segment((2.,0))
|
||||
.close()
|
||||
.arc((.6,.6),0.4,0.,360.)
|
||||
.assemble(tag='face')
|
||||
.edges('%LINE',tag='face')
|
||||
.vertices()
|
||||
.chamfer(0.2)
|
||||
)
|
||||
|
||||
Once the construction is finished it has to be converted to the face-based representation
|
||||
using :meth:`~cadquery.Sketch.assemble`. Afterwards, face based operations can be applied.
|
||||
|
||||
Convex hull
|
||||
===========
|
||||
|
||||
For certain special use-cases convex hull can be constructed from straight segments
|
||||
and circles.
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
result = (
|
||||
cq.Sketch()
|
||||
.arc((0,0),1.,0.,360.)
|
||||
.arc((1,1.5),0.5,0.,360.)
|
||||
.segment((0.,2),(-1,3.))
|
||||
.hull()
|
||||
)
|
||||
|
||||
Constraint-based sketches
|
||||
=========================
|
||||
|
||||
Finally, if desired, geometric constraints can be used to construct sketches. So
|
||||
far only line segments and arcs can be used in such a use case.
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
result = (
|
||||
cq.Sketch()
|
||||
.segment((0,0), (0,3.),"s1")
|
||||
.arc((0.,3.), (1.5,1.5), (0.,0.),"a1")
|
||||
.constrain("s1","Fixed",None)
|
||||
.constrain("s1", "a1","Coincident",None)
|
||||
.constrain("a1", "s1","Coincident",None)
|
||||
.constrain("s1",'a1', "Angle", 45)
|
||||
.solve()
|
||||
.assemble()
|
||||
)
|
||||
|
||||
Following constraints are implemented. Arguments are passed in as one tuple in :meth:`~cadquery.Sketch.constrain`. In this table, `0..1` refers to a float between 0 and 1 where 0 would create a constraint relative to the start of the element, and 1 the end.
|
||||
|
||||
.. list-table::
|
||||
:widths: 15 10 15 30 30
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Arity
|
||||
- Entities
|
||||
- Arguments
|
||||
- Description
|
||||
* - FixedPoint
|
||||
- 1
|
||||
- All
|
||||
- `None` for arc center or `0..1` for point on segment/arc
|
||||
- Specified point is fixed
|
||||
* - Coincident
|
||||
- 2
|
||||
- All
|
||||
- None
|
||||
- Specified points coincide
|
||||
* - Angle
|
||||
- 2
|
||||
- All
|
||||
- `angle`
|
||||
- Angle between the tangents of the two entities is fixed
|
||||
* - Length
|
||||
- 1
|
||||
- All
|
||||
- `length`
|
||||
- Specified entity has fixed length
|
||||
* - Distance
|
||||
- 2
|
||||
- All
|
||||
- `None or 0..1, None or 0..1, distance`
|
||||
- Distance between two points is fixed
|
||||
* - Radius
|
||||
- 1
|
||||
- Arc
|
||||
- `radius`
|
||||
- Specified entity has a fixed radius
|
||||
* - Orientation
|
||||
- 1
|
||||
- Segment
|
||||
- `x,y`
|
||||
- Specified entity is parallel to `(x,y)`
|
||||
* - ArcAngle
|
||||
- 1
|
||||
- Arc
|
||||
- `angle`
|
||||
- Specified entity is fixed angular span
|
||||
|
||||
|
||||
Workplane integration
|
||||
---------------------
|
||||
|
||||
Once created, a sketch can be used to construct various features on a workplane.
|
||||
Supported operations include :meth:`~cadquery.Workplane.extrude`,
|
||||
:meth:`~cadquery.Workplane.twistExtrude`, :meth:`~cadquery.Workplane.revolve`,
|
||||
:meth:`~cadquery.Workplane.sweep`, :meth:`~cadquery.Workplane.cutBlind`, :meth:`~cadquery.Workplane.cutThruAll` and :meth:`~cadquery.Workplane.loft`.
|
||||
|
||||
Sketches can be created as separate entities and reused, but also created ad-hoc
|
||||
in one fluent chain of calls as shown below.
|
||||
|
||||
|
||||
Note that the sketch is placed on all locations that are on the top of the stack.
|
||||
|
||||
Constructing sketches in-place can be accomplished as follows.
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
result = (
|
||||
cq.Workplane()
|
||||
.box(5,5,1)
|
||||
.faces('>Z')
|
||||
.sketch()
|
||||
.regularPolygon(2,3,tag='outer')
|
||||
.regularPolygon(1.5,3,mode='s')
|
||||
.vertices(tag='outer')
|
||||
.fillet(.2)
|
||||
.finalize()
|
||||
.extrude(.5)
|
||||
)
|
||||
|
||||
Sketch API is available after the :meth:`~cadquery.Workplane.sketch` call and original `workplane`.
|
||||
|
||||
When multiple elements are selected before constructing the sketch, multiple sketches will be created.
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
result = (
|
||||
cq.Workplane()
|
||||
.box(5,5,1)
|
||||
.faces('>Z')
|
||||
.workplane()
|
||||
.rarray(2,2,2,2)
|
||||
.rect(1.5,1.5)
|
||||
.extrude(.5)
|
||||
.faces('>Z')
|
||||
.sketch()
|
||||
.circle(0.4)
|
||||
.wires()
|
||||
.distribute(6)
|
||||
.circle(0.1,mode='a')
|
||||
.clean()
|
||||
.finalize()
|
||||
.cutBlind(-0.5,taper=10)
|
||||
)
|
||||
|
||||
Sometimes it is desired to reuse existing sketches and place them as-is on a workplane.
|
||||
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
s = (
|
||||
cq.Sketch()
|
||||
.trapezoid(3,1,110)
|
||||
.vertices()
|
||||
.fillet(0.2)
|
||||
)
|
||||
|
||||
result = (
|
||||
cq.Workplane()
|
||||
.box(5,5,5)
|
||||
.faces('>X')
|
||||
.workplane()
|
||||
.transformed((0,0,-90))
|
||||
.placeSketch(s)
|
||||
.cutThruAll()
|
||||
)
|
||||
|
||||
Reusing of existing sketches is needed when using :meth:`~cadquery.Workplane.loft`.
|
||||
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
from cadquery import Workplane, Sketch, Vector, Location
|
||||
|
||||
s1 = (
|
||||
Sketch()
|
||||
.trapezoid(3,1,110)
|
||||
.vertices()
|
||||
.fillet(0.2)
|
||||
)
|
||||
|
||||
s2 = (
|
||||
Sketch()
|
||||
.rect(2,1)
|
||||
.vertices()
|
||||
.fillet(0.2)
|
||||
)
|
||||
|
||||
result = (
|
||||
Workplane()
|
||||
.placeSketch(s1, s2.moved(Location(Vector(0, 0, 3))))
|
||||
.loft()
|
||||
)
|
||||
|
||||
When lofting only outer wires are taken into account.
|
||||
@ -26,3 +26,4 @@ dependencies:
|
||||
- pip:
|
||||
- "--editable=."
|
||||
- sphinxcadquery
|
||||
- multimethod
|
||||
|
||||
3
mypy.ini
3
mypy.ini
@ -1,5 +1,6 @@
|
||||
[mypy]
|
||||
ignore_missing_imports = False
|
||||
disable_error_code = no-redef
|
||||
|
||||
[mypy-ezdxf.*]
|
||||
ignore_missing_imports = True
|
||||
@ -28,3 +29,5 @@ ignore_missing_imports = True
|
||||
[mypy-docutils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-typish.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@ -105,7 +105,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
import OCP
|
||||
|
||||
self.assertEqual(type(r), OCP.TopoDS.TopoDS_Compound)
|
||||
self.assertEqual(type(r), OCP.TopoDS.TopoDS_Solid)
|
||||
|
||||
def testToSVG(self):
|
||||
"""
|
||||
@ -3443,6 +3443,10 @@ class TestCadQuery(BaseTest):
|
||||
with self.assertRaises(ValueError):
|
||||
Workplane().box(1, 1, 1).faces().circle(0.1).extrude(0.1)
|
||||
|
||||
# check that extruding nested geometry raises
|
||||
with self.assertRaises(ValueError):
|
||||
Workplane().rect(2, 2).rect(1, 1).extrude(2, taper=4)
|
||||
|
||||
def testTaperedExtrudeCutBlind(self):
|
||||
|
||||
h = 1.0
|
||||
@ -4942,6 +4946,80 @@ class TestCadQuery(BaseTest):
|
||||
self.assertTrue(p2.Contains(f2.Center().toPnt(), 0.1))
|
||||
self.assertTrue(Vector(p2.Axis().Direction()) == f2.normalAt())
|
||||
|
||||
def testEachpoint(self):
|
||||
|
||||
r1 = (
|
||||
Workplane(origin=(0, 0, 1))
|
||||
.add(
|
||||
[
|
||||
Vector(),
|
||||
Location(Vector(0, 0, -1,)),
|
||||
Sketch().rect(1, 1),
|
||||
Face.makePlane(1, 1),
|
||||
]
|
||||
)
|
||||
.eachpoint(lambda l: Face.makePlane(1, 1).locate(l))
|
||||
)
|
||||
|
||||
self.assertTrue(len(r1.objects) == 4)
|
||||
|
||||
for v in r1.vals():
|
||||
self.assertTupleAlmostEquals(v.Center().toTuple(), (0, 0, 0), 6)
|
||||
|
||||
def testSketch(self):
|
||||
|
||||
r1 = (
|
||||
Workplane()
|
||||
.box(10, 10, 1)
|
||||
.faces(">Z")
|
||||
.sketch()
|
||||
.slot(2, 1)
|
||||
.slot(2, 1, angle=90)
|
||||
.clean()
|
||||
.finalize()
|
||||
.extrude(1)
|
||||
)
|
||||
|
||||
self.assertTrue(r1.val().isValid())
|
||||
self.assertEqual(len(r1.faces().vals()), 19)
|
||||
|
||||
r2 = (
|
||||
Workplane()
|
||||
.sketch()
|
||||
.circle(2)
|
||||
.wires()
|
||||
.offset(0.1, mode="s")
|
||||
.finalize()
|
||||
.sketch()
|
||||
.rect(1, 1)
|
||||
.finalize()
|
||||
.extrude(1, taper=5)
|
||||
)
|
||||
|
||||
self.assertTrue(r2.val().isValid())
|
||||
self.assertEqual(len(r2.faces().vals()), 6)
|
||||
|
||||
r3 = (
|
||||
Workplane()
|
||||
.pushPoints((Location(Vector(1, 1, 0)),))
|
||||
.sketch()
|
||||
.circle(2)
|
||||
.wires()
|
||||
.offset(-0.1, mode="s")
|
||||
.finalize()
|
||||
.extrude(1)
|
||||
)
|
||||
|
||||
self.assertTrue(r3.val().isValid())
|
||||
self.assertEqual(len(r3.faces().vals()), 4)
|
||||
self.assertTupleAlmostEquals(r3.val().Center().toTuple(), (1, 1, 0.5), 6)
|
||||
|
||||
s = Sketch().trapezoid(3, 1, 120)
|
||||
|
||||
r4 = Workplane().placeSketch(s, s.moved(Location(Vector(0, 0, 3)))).loft()
|
||||
|
||||
self.assertEqual(len(r4.solids().vals()), 1)
|
||||
|
||||
def testCircumscribedPolygon(self):
|
||||
"""
|
||||
Test that circumscribed polygons result in the correct shapes
|
||||
|
||||
32
tests/test_hull.py
Normal file
32
tests/test_hull.py
Normal file
@ -0,0 +1,32 @@
|
||||
import pytest
|
||||
|
||||
import cadquery as cq
|
||||
from cadquery import hull
|
||||
|
||||
|
||||
def test_hull():
|
||||
|
||||
c1 = cq.Edge.makeCircle(0.5, (-1.5, 0.5, 0))
|
||||
c2 = cq.Edge.makeCircle(0.5, (1.9, 0.0, 0))
|
||||
c3 = cq.Edge.makeCircle(0.2, (0.3, 1.5, 0))
|
||||
c4 = cq.Edge.makeCircle(0.2, (1.0, 1.5, 0))
|
||||
c5 = cq.Edge.makeCircle(0.1, (0.0, 0.0, 0.0))
|
||||
e1 = cq.Edge.makeLine(cq.Vector(0, -0.5), cq.Vector(-0.5, 1.5))
|
||||
e2 = cq.Edge.makeLine(cq.Vector(2.1, 1.5), cq.Vector(2.6, 1.5))
|
||||
|
||||
edges = [c1, c2, c3, c4, c5, e1, e2]
|
||||
|
||||
h = hull.find_hull(edges)
|
||||
|
||||
assert len(h.Vertices()) == 11
|
||||
assert h.IsClosed()
|
||||
assert h.isValid()
|
||||
|
||||
|
||||
def test_validation():
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
e1 = cq.Edge.makeEllipse(2, 1)
|
||||
c1 = cq.Edge.makeCircle(0.5, (-1.5, 0.5, 0))
|
||||
hull.find_hull([c1, e1])
|
||||
588
tests/test_sketch.py
Normal file
588
tests/test_sketch.py
Normal file
@ -0,0 +1,588 @@
|
||||
import os
|
||||
|
||||
from cadquery.sketch import Sketch, Vector, Location
|
||||
from cadquery.selectors import LengthNthSelector
|
||||
|
||||
from pytest import approx, raises
|
||||
from math import pi, sqrt
|
||||
|
||||
testdataDir = os.path.join(os.path.dirname(__file__), "testdata")
|
||||
|
||||
|
||||
def test_face_interface():
|
||||
|
||||
s1 = Sketch().rect(1, 2, 45)
|
||||
|
||||
assert s1._faces.Area() == approx(2)
|
||||
assert s1.vertices(">X")._selection[0].toTuple()[0] == approx(1.5 / sqrt(2))
|
||||
|
||||
s2 = Sketch().circle(1)
|
||||
|
||||
assert s2._faces.Area() == approx(pi)
|
||||
|
||||
s3 = Sketch().ellipse(2, 0.5)
|
||||
|
||||
assert s3._faces.Area() == approx(pi)
|
||||
|
||||
s4 = Sketch().trapezoid(2, 0.5, 45)
|
||||
|
||||
assert s4._faces.Area() == approx(0.75)
|
||||
|
||||
s4 = Sketch().trapezoid(2, 0.5, 45)
|
||||
|
||||
assert s4._faces.Area() == approx(0.75)
|
||||
|
||||
s5 = Sketch().slot(3, 2)
|
||||
|
||||
assert s5._faces.Area() == approx(6 + pi)
|
||||
assert s5.edges(">Y")._selection[0].Length() == approx(3)
|
||||
|
||||
s6 = Sketch().regularPolygon(1, 5)
|
||||
|
||||
assert len(s6.vertices()._selection) == 5
|
||||
assert s6.vertices(">Y")._selection[0].toTuple()[1] == approx(1)
|
||||
|
||||
s7 = Sketch().polygon([(0, 0), (0, 1), (1, 0)])
|
||||
|
||||
assert len(s7.vertices()._selection) == 3
|
||||
assert s7._faces.Area() == approx(0.5)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().face(Sketch().rect(1, 1)._faces)
|
||||
|
||||
|
||||
def test_modes():
|
||||
|
||||
s1 = Sketch().rect(2, 2).rect(1, 1, mode="a")
|
||||
|
||||
assert s1._faces.Area() == approx(4)
|
||||
assert len(s1._faces.Faces()) == 2
|
||||
|
||||
s2 = Sketch().rect(2, 2).rect(1, 1, mode="s")
|
||||
|
||||
assert s2._faces.Area() == approx(4 - 1)
|
||||
assert len(s2._faces.Faces()) == 1
|
||||
|
||||
s3 = Sketch().rect(2, 2).rect(1, 1, mode="i")
|
||||
|
||||
assert s3._faces.Area() == approx(1)
|
||||
assert len(s3._faces.Faces()) == 1
|
||||
|
||||
s4 = Sketch().rect(2, 2).rect(1, 1, mode="c", tag="t")
|
||||
|
||||
assert s4._faces.Area() == approx(4)
|
||||
assert len(s4._faces.Faces()) == 1
|
||||
assert s4._tags["t"][0].Area() == approx(1)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().rect(2, 2).rect(1, 1, mode="c")
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().rect(2, 2).rect(1, 1, mode="dummy")
|
||||
|
||||
|
||||
def test_distribute():
|
||||
|
||||
s1 = Sketch().rarray(2, 2, 3, 3).rect(1, 1)
|
||||
|
||||
assert s1._faces.Area() == approx(9)
|
||||
assert len(s1._faces.Faces()) == 9
|
||||
|
||||
s2 = Sketch().parray(2, 0, 90, 3).rect(1, 1)
|
||||
|
||||
assert s2._faces.Area() == approx(3)
|
||||
assert len(s2._faces.Faces()) == 3
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().rarray(2, 2, 3, 0).rect(1, 1)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().parray(2, 0, 90, 0).rect(1, 1)
|
||||
|
||||
s3 = Sketch().circle(4, mode="c", tag="c").edges(tag="c").distribute(3).rect(1, 1)
|
||||
|
||||
assert s2._faces.Area() == approx(3)
|
||||
assert len(s3._faces.Faces()) == 3
|
||||
assert len(s3.reset().vertices("<X")._selection) == 2
|
||||
|
||||
for f in s3._faces.Faces():
|
||||
assert f.Center().Length == approx(4)
|
||||
|
||||
s4 = (
|
||||
Sketch()
|
||||
.circle(4, mode="c", tag="c")
|
||||
.edges(tag="c")
|
||||
.distribute(3, rotate=False)
|
||||
.rect(1, 1)
|
||||
)
|
||||
|
||||
assert s4._faces.Area() == approx(3)
|
||||
assert len(s4._faces.Faces()) == 3
|
||||
assert len(s4.reset().vertices("<X")._selection) == 4
|
||||
|
||||
for f in s4._faces.Faces():
|
||||
assert f.Center().Length == approx(4)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().rect(2, 2).faces().distribute(5)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().rect(2, 2).distribute(5)
|
||||
|
||||
s5 = Sketch().push([(0, 0), (1, 1)]).rarray(2, 2, 3, 3).rect(0.5, 0.5)
|
||||
|
||||
assert s5._faces.Area() == approx(18 * 0.25)
|
||||
assert len(s5._faces.Faces()) == 18
|
||||
|
||||
s6 = Sketch().push([(0, 0), (1, 1)]).parray(2, 0, 90, 3).rect(0.5, 0.5)
|
||||
|
||||
assert s6._faces.Area() == approx(6 * 0.25)
|
||||
assert len(s6._faces.Faces()) == 6
|
||||
|
||||
s7 = Sketch().parray(2, 0, 90, 3, False).rect(0.5, 0.5).reset().vertices(">(1,1,0)")
|
||||
|
||||
assert len(s7._selection) == 1
|
||||
|
||||
s8 = Sketch().push([(0, 0), (0, 1)]).parray(2, 0, 90, 3).rect(0.5, 0.5)
|
||||
s8.reset().faces(">(1,1,0)")
|
||||
|
||||
assert s8._selection[0].Center().Length == approx(3)
|
||||
|
||||
s9 = Sketch().push([(0, 1)], tag="loc")
|
||||
|
||||
assert len(s9._tags["loc"]) == 1
|
||||
|
||||
|
||||
def test_each():
|
||||
|
||||
s1 = Sketch().each(lambda l: Sketch().push([l]).rect(1, 1))
|
||||
|
||||
assert len(s1._faces.Faces()) == 1
|
||||
|
||||
s2 = (
|
||||
Sketch()
|
||||
.push([(0, 0), (2, 2)])
|
||||
.each(lambda l: Sketch().push([l]).rect(1, 1), ignore_selection=True)
|
||||
)
|
||||
|
||||
assert len(s2._faces.Faces()) == 1
|
||||
|
||||
|
||||
def test_modifiers():
|
||||
|
||||
s1 = Sketch().push([(-2, 0), (2, 0)]).rect(1, 1).reset().vertices("<X").fillet(0.1)
|
||||
|
||||
assert len(s1._faces.Faces()) == 2
|
||||
assert len(s1._faces.Edges()) == 10
|
||||
|
||||
s2 = Sketch().push([(-2, 0), (2, 0)]).rect(1, 1).reset().vertices(">X").chamfer(0.1)
|
||||
|
||||
assert len(s2._faces.Faces()) == 2
|
||||
assert len(s2._faces.Edges()) == 10
|
||||
|
||||
s3 = Sketch().push([(-2, 0), (2, 0)]).rect(1, 1).reset().hull()
|
||||
|
||||
assert len(s3._faces.Faces()) == 3
|
||||
assert s3._faces.Area() == approx(5)
|
||||
|
||||
s4 = Sketch().push([(-2, 0), (2, 0)]).rect(1, 1).reset().hull()
|
||||
|
||||
assert len(s4._faces.Faces()) == 3
|
||||
assert s4._faces.Area() == approx(5)
|
||||
|
||||
s5 = (
|
||||
Sketch()
|
||||
.push([(-2, 0), (0, 0), (2, 0)])
|
||||
.rect(1, 1)
|
||||
.reset()
|
||||
.faces("not >X")
|
||||
.edges()
|
||||
.hull()
|
||||
)
|
||||
|
||||
assert len(s5._faces.Faces()) == 4
|
||||
assert s5._faces.Area() == approx(4)
|
||||
|
||||
s6 = Sketch().segment((0, 0), (0, 1)).segment((1, 0), (2, 0)).hull()
|
||||
|
||||
assert len(s6._faces.Faces()) == 1
|
||||
assert s6._faces.Area() == approx(1)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().rect(1, 1).vertices().hull()
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().hull()
|
||||
|
||||
s7 = Sketch().rect(2, 2).wires().offset(1)
|
||||
|
||||
assert len(s7._faces.Faces()) == 2
|
||||
assert len(s7._faces.Edges()) == 4 + 4 + 4
|
||||
|
||||
s7.clean()
|
||||
|
||||
assert len(s7._faces.Faces()) == 1
|
||||
assert len(s7._faces.Edges()) == 4 + 4
|
||||
|
||||
s8 = Sketch().rect(2, 2).wires().offset(-0.5, mode="s")
|
||||
|
||||
assert len(s8._faces.Faces()) == 1
|
||||
assert len(s8._faces.Edges()) == 4 + 4
|
||||
|
||||
|
||||
def test_delete():
|
||||
|
||||
s1 = Sketch().push([(-2, 0), (2, 0)]).rect(1, 1).reset()
|
||||
|
||||
assert len(s1._faces.Faces()) == 2
|
||||
|
||||
s1.faces("<X").delete()
|
||||
|
||||
assert len(s1._faces.Faces()) == 1
|
||||
|
||||
s2 = Sketch().segment((0, 0), (1, 0)).segment((0, 1), tag="e").close()
|
||||
assert len(s2._edges) == 3
|
||||
|
||||
s2.edges("<X").delete()
|
||||
|
||||
assert len(s2._edges) == 2
|
||||
|
||||
|
||||
def test_selectors():
|
||||
|
||||
s = Sketch().push([(-2, 0), (2, 0)]).rect(1, 1).rect(0.5, 0.5, mode="s").reset()
|
||||
|
||||
assert len(s._selection) == 0
|
||||
|
||||
s.vertices()
|
||||
|
||||
assert len(s._selection) == 16
|
||||
|
||||
s.reset()
|
||||
|
||||
assert len(s._selection) == 0
|
||||
|
||||
s.edges()
|
||||
|
||||
assert len(s._selection) == 16
|
||||
|
||||
s.reset().wires()
|
||||
|
||||
assert len(s._selection) == 4
|
||||
|
||||
s.reset().faces()
|
||||
|
||||
assert len(s._selection) == 2
|
||||
|
||||
s.reset().vertices("<Y")
|
||||
|
||||
assert len(s._selection) == 4
|
||||
|
||||
s.reset().edges("<X or >X")
|
||||
|
||||
assert len(s._selection) == 2
|
||||
|
||||
s.tag("test").reset()
|
||||
|
||||
assert len(s._selection) == 0
|
||||
|
||||
s.select("test")
|
||||
|
||||
assert len(s._selection) == 2
|
||||
|
||||
s.reset().wires()
|
||||
|
||||
assert len(s._selection) == 4
|
||||
|
||||
s.reset().wires(LengthNthSelector(1))
|
||||
|
||||
assert len(s._selection) == 2
|
||||
|
||||
|
||||
def test_edge_interface():
|
||||
|
||||
s1 = (
|
||||
Sketch()
|
||||
.segment((0, 0), (1, 0))
|
||||
.segment((1, 1))
|
||||
.segment(1, 180)
|
||||
.close()
|
||||
.assemble()
|
||||
)
|
||||
|
||||
assert len(s1._faces.Faces()) == 1
|
||||
assert s1._faces.Area() == approx(1)
|
||||
|
||||
s2 = Sketch().arc((0, 0), (1, 1), (0, 2)).close().assemble()
|
||||
|
||||
assert len(s2._faces.Faces()) == 1
|
||||
assert s2._faces.Area() == approx(pi / 2)
|
||||
|
||||
s3 = Sketch().arc((0, 0), (1, 1), (0, 2)).arc((-1, 1), (0, 0)).assemble()
|
||||
|
||||
assert len(s3._faces.Faces()) == 1
|
||||
assert s3._faces.Area() == approx(pi)
|
||||
|
||||
s4 = Sketch().arc((0, 0), 1, 0, 90)
|
||||
|
||||
assert len(s4.vertices()._selection) == 2
|
||||
assert s4.vertices(">Y")._selection[0].Center().y == approx(1)
|
||||
|
||||
s5 = Sketch().arc((0, 0), 1, 0, -90)
|
||||
|
||||
assert len(s5.vertices()._selection) == 2
|
||||
assert s5.vertices(">Y")._selection[0].Center().y == approx(0)
|
||||
|
||||
s6 = Sketch().arc((0, 0), 1, 90, 360)
|
||||
|
||||
assert len(s6.vertices()._selection) == 1
|
||||
|
||||
|
||||
def test_assemble():
|
||||
|
||||
s1 = Sketch()
|
||||
s1.segment((0.0, 0), (0.0, 2.0))
|
||||
s1.segment(Vector(4.0, -1)).close().arc((0.7, 0.6), 0.4, 0.0, 360.0).assemble()
|
||||
|
||||
|
||||
def test_finalize():
|
||||
|
||||
parent = object()
|
||||
s = Sketch(parent).rect(2, 2).circle(0.5, mode="s")
|
||||
|
||||
assert s.finalize() is parent
|
||||
|
||||
|
||||
def test_misc():
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch()._startPoint()
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch()._endPoint()
|
||||
|
||||
|
||||
def test_located():
|
||||
|
||||
s1 = Sketch().segment((0, 0), (1, 0)).segment((1, 1)).close().assemble()
|
||||
|
||||
assert len(s1._edges) == 3
|
||||
assert len(s1._faces.Faces()) == 1
|
||||
|
||||
s2 = s1.located(loc=Location())
|
||||
|
||||
assert len(s2._edges) == 0
|
||||
assert len(s2._faces.Faces()) == 1
|
||||
|
||||
|
||||
def test_constraint_validation():
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().segment(1.0, 1.0, "s").constrain("s", "Dummy", None)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().segment(1.0, 1.0, "s").constrain("s", "s", "Fixed", None)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().spline([(1.0, 1.0), (2.0, 1.0), (0.0, 0.0)], "s").constrain(
|
||||
"s", "Fixed", None
|
||||
)
|
||||
|
||||
with raises(ValueError):
|
||||
Sketch().segment(1.0, 1.0, "s").constrain("s", "Fixed", 1)
|
||||
|
||||
|
||||
def test_constraint_solver():
|
||||
|
||||
s1 = (
|
||||
Sketch()
|
||||
.segment((0.0, 0), (0.0, 2.0), "s1")
|
||||
.segment((0.5, 2.5), (1.0, 1), "s2")
|
||||
.close("s3")
|
||||
)
|
||||
s1.constrain("s1", "Fixed", None)
|
||||
s1.constrain("s1", "s2", "Coincident", None)
|
||||
s1.constrain("s2", "s3", "Coincident", None)
|
||||
s1.constrain("s3", "s1", "Coincident", None)
|
||||
s1.constrain("s3", "s1", "Angle", 90)
|
||||
s1.constrain("s2", "s3", "Angle", 180 - 45)
|
||||
|
||||
s1.solve()
|
||||
|
||||
assert s1._solve_status["status"] == 4
|
||||
|
||||
s1.assemble()
|
||||
|
||||
assert s1._faces.isValid()
|
||||
|
||||
s2 = (
|
||||
Sketch()
|
||||
.arc((0.0, 0.0), (-0.5, 0.5), (0.0, 1.0), "a1")
|
||||
.arc((0.0, 1.0), (0.5, 1.5), (1.0, 1.0), "a2")
|
||||
.segment((1.0, 0.0), "s1")
|
||||
.close("s2")
|
||||
)
|
||||
|
||||
s2.constrain("s2", "Fixed", None)
|
||||
s2.constrain("s1", "s2", "Coincident", None)
|
||||
s2.constrain("a2", "s1", "Coincident", None)
|
||||
s2.constrain("s2", "a1", "Coincident", None)
|
||||
s2.constrain("a1", "a2", "Coincident", None)
|
||||
s2.constrain("s1", "s2", "Angle", 90)
|
||||
s2.constrain("s2", "a1", "Angle", 90)
|
||||
s2.constrain("a1", "a2", "Angle", -90)
|
||||
s2.constrain("a2", "s1", "Angle", 90)
|
||||
s2.constrain("s1", "Length", 0.5)
|
||||
s2.constrain("a1", "Length", 1.0)
|
||||
|
||||
s2.solve()
|
||||
|
||||
assert s2._solve_status["status"] == 4
|
||||
|
||||
s2.assemble()
|
||||
|
||||
assert s2._faces.isValid()
|
||||
|
||||
s2._tags["s1"][0].Length() == approx(0.5)
|
||||
s2._tags["a1"][0].Length() == approx(1.0)
|
||||
|
||||
s3 = (
|
||||
Sketch()
|
||||
.arc((0.0, 0.0), (-0.5, 0.5), (0.0, 1.0), "a1")
|
||||
.segment((1.0, 0.0), "s1")
|
||||
.close("s2")
|
||||
)
|
||||
|
||||
s3.constrain("s2", "Fixed", None)
|
||||
s3.constrain("a1", "ArcAngle", 60)
|
||||
s3.constrain("a1", "Radius", 1.0)
|
||||
s3.constrain("s2", "a1", "Coincident", None)
|
||||
s3.constrain("a1", "s1", "Coincident", None)
|
||||
s3.constrain("s1", "s2", "Coincident", None)
|
||||
|
||||
s3.solve()
|
||||
|
||||
assert s3._solve_status["status"] == 4
|
||||
|
||||
s3.assemble()
|
||||
|
||||
assert s3._faces.isValid()
|
||||
|
||||
s3._tags["a1"][0].radius() == approx(1)
|
||||
s3._tags["a1"][0].Length() == approx(pi / 3)
|
||||
|
||||
s4 = (
|
||||
Sketch()
|
||||
.arc((0.0, 0.0), (-0.5, 0.5), (0.0, 1.0), "a1")
|
||||
.segment((1.0, 0.0), "s1")
|
||||
.close("s2")
|
||||
)
|
||||
|
||||
s4.constrain("s2", "Fixed", None)
|
||||
s4.constrain("s1", "Orientation", (-1.0, -1))
|
||||
s4.constrain("s1", "s2", "Distance", (0.0, 0.5, 2.0))
|
||||
s4.constrain("s2", "a1", "Coincident", None)
|
||||
s4.constrain("a1", "s1", "Coincident", None)
|
||||
s4.constrain("s1", "s2", "Coincident", None)
|
||||
|
||||
s4.solve()
|
||||
|
||||
assert s4._solve_status["status"] == 4
|
||||
|
||||
s4.assemble()
|
||||
|
||||
assert s4._faces.isValid()
|
||||
|
||||
seg1 = s4._tags["s1"][0]
|
||||
seg2 = s4._tags["s2"][0]
|
||||
|
||||
assert (seg1.endPoint() - seg1.startPoint()).getAngle(Vector(-1, -1)) == approx(
|
||||
0, abs=1e-9
|
||||
)
|
||||
|
||||
midpoint = (seg2.startPoint() + seg2.endPoint()) / 2
|
||||
|
||||
(midpoint - seg1.startPoint()).Length == approx(2)
|
||||
|
||||
s5 = (
|
||||
Sketch()
|
||||
.segment((0, 0), (0, 3.0), "s1")
|
||||
.arc((0.0, 0), (1.5, 1.5), (0.0, 3), "a1")
|
||||
.arc((0.0, 0), (-1.0, 1.5), (0.0, 3), "a2")
|
||||
)
|
||||
|
||||
s5.constrain("s1", "Fixed", None)
|
||||
s5.constrain("s1", "a1", "Distance", (0.5, 0.5, 3))
|
||||
s5.constrain("s1", "a1", "Distance", (0.0, 1.0, 0.0))
|
||||
s5.constrain("a1", "s1", "Distance", (0.0, 1.0, 0.0))
|
||||
s5.constrain("s1", "a2", "Coincident", None)
|
||||
s5.constrain("a2", "s1", "Coincident", None)
|
||||
s5.constrain("a1", "a2", "Distance", (0.5, 0.5, 10.5))
|
||||
|
||||
s5.solve()
|
||||
|
||||
assert s5._solve_status["status"] == 4
|
||||
|
||||
mid0 = s5._edges[0].positionAt(0.5)
|
||||
mid1 = s5._edges[1].positionAt(0.5)
|
||||
mid2 = s5._edges[2].positionAt(0.5)
|
||||
|
||||
assert (mid1 - mid0).Length == approx(3)
|
||||
assert (mid1 - mid2).Length == approx(10.5)
|
||||
|
||||
s6 = (
|
||||
Sketch()
|
||||
.segment((0, 0), (0, 3.0), "s1")
|
||||
.arc((0.0, 0), (5.5, 5.5), (0.0, 3), "a1")
|
||||
)
|
||||
|
||||
s6.constrain("s1", "Fixed", None)
|
||||
s6.constrain("s1", "a1", "Coincident", None)
|
||||
s6.constrain("a1", "s1", "Coincident", None)
|
||||
s6.constrain("a1", "s1", "Distance", (None, 0.5, 0))
|
||||
|
||||
s6.solve()
|
||||
|
||||
assert s6._solve_status["status"] == 4
|
||||
|
||||
mid0 = s6._edges[0].positionAt(0.5)
|
||||
mid1 = s6._edges[1].positionAt(0.5)
|
||||
|
||||
assert (mid1 - mid0).Length == approx(1.5)
|
||||
|
||||
s7 = (
|
||||
Sketch()
|
||||
.segment((0, 0), (0, 3.0), "s1")
|
||||
.arc((0.0, 0), (5.5, 5.5), (0.0, 4), "a1")
|
||||
)
|
||||
|
||||
s7.constrain("s1", "FixedPoint", 0)
|
||||
s7.constrain("a1", "FixedPoint", None)
|
||||
s7.constrain("a1", "FixedPoint", 1)
|
||||
s7.constrain("a1", "s1", "Distance", (0, 0, 0))
|
||||
s7.constrain("a1", "s1", "Distance", (1, 1, 0))
|
||||
|
||||
s7.solve()
|
||||
|
||||
assert s7._solve_status["status"] == 4
|
||||
|
||||
s7.assemble()
|
||||
|
||||
assert s7._faces.isValid()
|
||||
|
||||
|
||||
def test_dxf_import():
|
||||
|
||||
filename = os.path.join(testdataDir, "gear.dxf")
|
||||
|
||||
s1 = Sketch().importDXF(filename, tol=1e-3)
|
||||
|
||||
assert s1._faces.isValid()
|
||||
|
||||
s2 = Sketch().importDXF(filename, tol=1e-3).circle(5, mode="s")
|
||||
|
||||
assert s2._faces.isValid()
|
||||
|
||||
s3 = Sketch().circle(20).importDXF(filename, tol=1e-3, mode="s")
|
||||
|
||||
assert s3._faces.isValid()
|
||||
Reference in New Issue
Block a user