Changed bounding box to Vectors, Added Joints tests

This commit is contained in:
Roger Maitland 2023-02-08 13:38:03 -05:00
parent bf3aaefe10
commit 4500d333de
12 changed files with 408 additions and 156 deletions

View file

@ -54,7 +54,7 @@ with BuildSketch() as example_6:
Mirror(about=Plane.YZ)
MakeFace()
# [Ex. 6]
Scale(by=2 / example_6.sketch.bounding_box().ylen)
Scale(by=2 / example_6.sketch.bounding_box().size.Y)
example_6.sketch.export_svg(
"assets/buildline_example_6.svg", (0, 0, 1), (0, 1, 0), svg_opts=svg_opts1
)

View file

@ -58,7 +58,7 @@ class Hinge(Compound):
):
# The profile of the hinge used to create the tabs
with BuildPart() as hinge_profile:
with BuildPart(Plane.XY, mode=Mode.PRIVATE) as hinge_profile:
with BuildSketch():
for i, loc in enumerate(
GridLocations(0, length / 5, 1, 5, align=(Align.MIN, Align.MIN))
@ -74,7 +74,7 @@ class Hinge(Compound):
Extrude(amount=-barrel_diameter)
# The hinge pin
with BuildPart() as pin:
with BuildPart(Plane.XY, mode=Mode.PRIVATE) as pin:
Cylinder(
radius=pin_diameter / 2,
height=length,
@ -92,7 +92,7 @@ class Hinge(Compound):
)
# Either the external and internal leaf with joints
with BuildPart() as leaf_builder:
with BuildPart(Plane.XY, mode=Mode.PRIVATE) as leaf_builder:
with BuildSketch():
with BuildLine():
l1 = Line((0, 0), (width - barrel_diameter / 2, 0))
@ -162,7 +162,7 @@ class Hinge(Compound):
label="hole" + str(hole),
to_part=leaf_builder.part,
axis=hole_location.to_axis(),
linear_range=(0, 2 * CM),
linear_range=(-2 * CM, 0),
angular_range=(0, 360),
)
@ -263,7 +263,7 @@ Compound.make_compound([m6_screw, m6_joint.symbol]).export_svg(
box.joints["hinge_attachment"].connect_to(hinge_outer.joints["leaf"])
hinge_outer.joints["hinge_axis"].connect_to(hinge_inner.joints["hinge_axis"], angle=120)
hinge_inner.joints["leaf"].connect_to(lid.joints["hinge_attachment"])
hinge_outer.joints["hole2"].connect_to(m6_joint, position=5, angle=30)
hinge_outer.joints["hole2"].connect_to(m6_joint, position=-5 * MM, angle=30)
Compound.make_compound([box, hinge_outer]).export_svg(
"tutorial_joint_box_outer.svg", (-100, -100, 50), (0, 0, 1), svg_opts=svg_opts

View file

@ -62,7 +62,7 @@ class Club(BaseSketchObject):
b3 = Bezier(b2 @ 1, (92, 57), (113, 188), (0, 188))
Mirror(about=Plane.YZ)
MakeFace()
Scale(by=height / club.sketch.bounding_box().ylen)
Scale(by=height / club.sketch.bounding_box().size.Y)
# Pass the shape to the BaseSketchObject class to create a new Club object
super().__init__(face=club.sketch, rotation=rotation, align=align, mode=mode)
@ -84,7 +84,7 @@ class Spade(BaseSketchObject):
l0 = Line(b2 @ 1, (0, -198))
Mirror(about=Plane.YZ)
MakeFace()
Scale(by=height / spade.sketch.bounding_box().ylen)
Scale(by=height / spade.sketch.bounding_box().size.Y)
super().__init__(face=spade.sketch, rotation=rotation, align=align, mode=mode)
@ -105,7 +105,7 @@ class Heart(BaseSketchObject):
b5 = Bezier(b4 @ 1, (40, -128), (0, -198))
Mirror(about=Plane.YZ)
MakeFace()
Scale(by=height / heart.sketch.bounding_box().ylen)
Scale(by=height / heart.sketch.bounding_box().size.Y)
super().__init__(face=heart.sketch, rotation=rotation, align=align, mode=mode)
@ -123,7 +123,7 @@ class Diamond(BaseSketchObject):
Mirror(about=Plane.XZ)
Mirror(about=Plane.YZ)
MakeFace()
Scale(by=height / diamond.sketch.bounding_box().ylen)
Scale(by=height / diamond.sketch.bounding_box().size.Y)
super().__init__(face=diamond.sketch, rotation=rotation, align=align, mode=mode)

View file

@ -26,6 +26,7 @@ license:
"""
from __future__ import annotations
import inspect
import contextvars
from itertools import product
from abc import ABC, abstractmethod, abstractstaticmethod
@ -172,7 +173,7 @@ class Builder(ABC):
@property
def max_dimension(self) -> float:
"""Maximum size of object in all directions"""
return self._obj.bounding_box().diagonal_length() if self._obj else 0.0
return self._obj.bounding_box().diagonal if self._obj else 0.0
@abstractmethod
def _add_to_context(
@ -192,6 +193,12 @@ class Builder(ABC):
here to avoid having to recreate this method.
"""
result = cls._current.get(None)
logger.info(
"Context requested by %s",
type(inspect.currentframe().f_back.f_locals["self"]).__name__,
)
if caller is not None and result is None:
if hasattr(caller, "_applies_to"):
raise RuntimeError(

View file

@ -194,11 +194,11 @@ class BoundingBox(Compound):
bounding_box = obj.bounding_box()
new_objects.append(
Solid.make_box(
bounding_box.xlen,
bounding_box.ylen,
bounding_box.zlen,
bounding_box.size.X,
bounding_box.size.Y,
bounding_box.size.Z,
Plane(
(bounding_box.xmin, bounding_box.ymin, bounding_box.zmin)
(bounding_box.min.X, bounding_box.min.Y, bounding_box.min.Z)
),
)
)
@ -212,11 +212,11 @@ class BoundingBox(Compound):
continue
bounding_box = obj.bounding_box()
vertices = [
(bounding_box.xmin, bounding_box.ymin),
(bounding_box.xmin, bounding_box.ymax),
(bounding_box.xmax, bounding_box.ymax),
(bounding_box.xmax, bounding_box.ymin),
(bounding_box.xmin, bounding_box.ymin),
(bounding_box.min.X, bounding_box.min.Y),
(bounding_box.min.X, bounding_box.max.Y),
(bounding_box.max.X, bounding_box.max.Y),
(bounding_box.max.X, bounding_box.min.Y),
(bounding_box.min.X, bounding_box.min.Y),
]
new_faces.append(
Face.make_from_wires(
@ -557,7 +557,7 @@ class Split(Compound):
new_objects = []
for obj in objects:
max_size = obj.bounding_box().diagonal_length()
max_size = obj.bounding_box().diagonal
cutters = []
if keep == Keep.BOTH:

View file

@ -279,11 +279,13 @@ class BasePartObject(Compound):
align_offset = []
for i in range(3):
if align[i] == Align.MIN:
align_offset.append(-bbox.mins[i])
align_offset.append(-bbox.min.to_tuple()[i])
elif align[i] == Align.CENTER:
align_offset.append(-(bbox.mins[i] + bbox.maxs[i]) / 2)
align_offset.append(
-(bbox.min.to_tuple()[i] + bbox.max.to_tuple()[i]) / 2
)
elif align[i] == Align.MAX:
align_offset.append(-bbox.maxs[i])
align_offset.append(-bbox.max.to_tuple()[i])
solid.move(Location(Vector(*align_offset)))
new_solids = [
@ -659,7 +661,7 @@ class Section(Compound):
self.section_height = height
self.mode = mode
max_size = context.part.bounding_box().diagonal_length()
max_size = context.part.bounding_box().diagonal
section_planes = (
section_by if section_by else WorkplaneList._get_context().workplanes

View file

@ -324,11 +324,11 @@ class BaseSketchObject(Compound):
align_offset = []
for i in range(2):
if align[i] == Align.MIN:
align_offset.append(-bbox.mins[i])
align_offset.append(-bbox.min.to_tuple()[i])
elif align[i] == Align.CENTER:
align_offset.append(-(bbox.mins[i] + bbox.maxs[i]) / 2)
align_offset.append(-(bbox.min.to_tuple()[i] + bbox.max.to_tuple()[i]) / 2)
elif align[i] == Align.MAX:
align_offset.append(-bbox.maxs[i])
align_offset.append(-bbox.max.to_tuple()[i])
else:
align_offset = [0, 0]

View file

@ -896,43 +896,30 @@ class Axis:
class BoundBox:
"""A BoundingBox for an object or set of objects. Wraps the OCP one"""
"""A BoundingBox for a Shape"""
def __init__(self, bounding_box: Bnd_Box) -> None:
self.wrapped: Bnd_Box = bounding_box
x_min, y_min, z_min, x_max, y_max, z_max = bounding_box.Get()
self.min = Vector(x_min, y_min, z_min)
self.max = Vector(x_max, y_max, z_max)
self.size = Vector(x_max - x_min, y_max - y_min, z_max - z_min)
self.xmin = x_min
self.xmax = x_max
self.xlen = x_max - x_min
self.ymin = y_min
self.ymax = y_max
self.ylen = y_max - y_min
self.zmin = z_min
self.zmax = z_max
self.zlen = z_max - z_min
self.mins = [x_min, y_min, z_min]
self.maxs = [x_max, y_max, z_max]
self.lens = [self.xlen, self.ylen, self.zlen]
@property
def diagonal(self) -> float:
"""body diagonal length (i.e. object maximum size)"""
return self.wrapped.SquareExtent() ** 0.5
def __repr__(self):
"""Display bounding box parameters"""
return (
f"bbox: {self.xmin} <= x <= {self.xmax}, {self.ymin} <= y <= {self.ymax}, "
f"{self.zmin} <= z <= {self.zmax}"
f"bbox: {self.min.X} <= x <= {self.max.X}, {self.min.Y} <= y <= {self.max.Y}, "
f"{self.min.Z} <= z <= {self.max.Z}"
)
def center(self) -> Vector:
"""Return center of the bounding box"""
return Vector(
(self.xmax + self.xmin) / 2,
(self.ymax + self.ymin) / 2,
(self.zmax + self.zmin) / 2,
)
def diagonal_length(self) -> float:
"""diagonal length (i.e. object maximum size)"""
return self.wrapped.SquareExtent() ** 0.5
return (self.min + self.max) / 2
def add(
self,
@ -996,22 +983,22 @@ class BoundBox:
"""
if (
bb1.xmin < bb2.xmin
and bb1.xmax > bb2.xmax
and bb1.ymin < bb2.ymin
and bb1.ymax > bb2.ymax
bb1.min.X < bb2.min.X
and bb1.max.X > bb2.max.X
and bb1.min.Y < bb2.min.Y
and bb1.max.Y > bb2.max.Y
):
return bb1
if (
bb2.xmin < bb1.xmin
and bb2.xmax > bb1.xmax
and bb2.ymin < bb1.ymin
and bb2.ymax > bb1.ymax
result = bb1
elif (
bb2.min.X < bb1.min.X
and bb2.max.X > bb1.max.X
and bb2.min.Y < bb1.min.Y
and bb2.max.Y > bb1.max.Y
):
return bb2
return None
result = bb2
else:
result = None
return result
@classmethod
def _from_topo_ds(
@ -1056,12 +1043,12 @@ class BoundBox:
"""
return not (
second_box.xmin > self.xmin
and second_box.ymin > self.ymin
and second_box.zmin > self.zmin
and second_box.xmax < self.xmax
and second_box.ymax < self.ymax
and second_box.zmax < self.zmax
second_box.min.X > self.min.X
and second_box.min.Y > self.min.Y
and second_box.min.Z > self.min.Z
and second_box.max.X < self.max.X
and second_box.max.Y < self.max.Y
and second_box.max.Z < self.max.Z
)
@ -1092,7 +1079,6 @@ class Color:
"""
def __init__(self, *args, **kwargs):
if len(args) == 1:
self.wrapped = Quantity_ColorRGBA()
exists = Quantity_ColorRGBA.ColorFromName_s(args[0], self.wrapped)
@ -1202,7 +1188,8 @@ class Location:
self, translation: VectorLike, rotation: RotationLike = None
): # pragma: no cover
"""Location with translation with respect to the original location.
If rotation is not None then the location includes the rotation (see also Rotation class)"""
If rotation is not None then the location includes the rotation (see also Rotation class)
"""
@overload
def __init__(self, plane: Plane): # pragma: no cover
@ -1229,7 +1216,6 @@ class Location:
with respect to the original location."""
def __init__(self, *args):
transform = gp_Trsf()
if len(args) == 0:
@ -1315,7 +1301,6 @@ class Location:
return Location(self.wrapped * other.wrapped)
def __pow__(self, exponent: int) -> Location:
return Location(self.wrapped.Powered(exponent))
def to_axis(self) -> Axis:
@ -1421,7 +1406,6 @@ class Matrix:
...
def __init__(self, matrix=None):
if matrix is None:
self.wrapped = gp_GTrsf()
elif isinstance(matrix, gp_GTrsf):
@ -1838,7 +1822,6 @@ class Mixin1D:
return_value: Union[Mixin1D, list[Mixin1D]]
if closest:
dist_calc = BRepExtrema_DistShapeShape()
dist_calc.LoadS1(self.wrapped)
@ -1939,7 +1922,7 @@ class Mixin3D:
if not self.is_valid():
raise ValueError("Invalid Shape")
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal_length(), 0)
max_radius = __max_fillet(0.0, 2 * self.bounding_box().diagonal, 0)
return max_radius
@ -2825,7 +2808,6 @@ class Shape(NodeMixin):
return tcast(Shapes, shape_LUT[shapetype(self.wrapped)])
def _entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
out = {} # using dict to prevent duplicates
explorer = TopExp_Explorer(self.wrapped, inverse_shape_LUT[topo_type])
@ -2842,7 +2824,6 @@ class Shape(NodeMixin):
def _entities_from(
self, child_type: Shapes, parent_type: Shapes
) -> Dict[Shape, list[Shape]]:
res = TopTools_IndexedDataMapOfShapeListOfShape()
TopTools_IndexedDataMapOfShapeListOfShape()
@ -3601,7 +3582,7 @@ class Shape(NodeMixin):
projected_faces = []
for text_face in text_faces:
bbox = text_face.bounding_box()
face_center_x = (bbox.xmin + bbox.xmax) / 2
face_center_x = (bbox.min.X + bbox.max.X) / 2
relative_position_on_wire = start + face_center_x / path_length
path_position = path.position_at(relative_position_on_wire)
path_tangent = path.tangent_at(relative_position_on_wire)
@ -4358,8 +4339,8 @@ class Plane:
elif isinstance(obj, Shape):
return_value = obj.transform_shape(transform_matrix)
elif isinstance(obj, BoundBox):
global_bottom_left = Vector(obj.xmin, obj.ymin, obj.zmin)
global_top_right = Vector(obj.xmax, obj.ymax, obj.zmax)
global_bottom_left = Vector(obj.min.X, obj.min.Y, obj.min.Z)
global_top_right = Vector(obj.max.X, obj.max.Y, obj.max.Z)
local_bottom_left = global_bottom_left.transform(transform_matrix)
local_top_right = global_top_right.transform(transform_matrix)
local_bbox = Bnd_Box(
@ -4671,7 +4652,7 @@ class Compound(Shape, Mixin3D):
relative to the path. Global coordinates to position the face.
"""
bbox = orig_face.bounding_box()
face_bottom_center = Vector((bbox.xmin + bbox.xmax) / 2, 0, 0)
face_bottom_center = Vector((bbox.min.X + bbox.max.X) / 2, 0, 0)
relative_position_on_wire = (
position_on_path + face_bottom_center.X / path_length
)
@ -4717,11 +4698,13 @@ class Compound(Shape, Mixin3D):
align_offset = []
for i in range(2):
if align[i] == Align.MIN:
align_offset.append(-bbox.mins[i])
align_offset.append(-bbox.min.to_tuple()[i])
elif align[i] == Align.CENTER:
align_offset.append(-(bbox.mins[i] + bbox.maxs[i]) / 2)
align_offset.append(
-(bbox.min.to_tuple()[i] + bbox.max.to_tuple()[i]) / 2
)
elif align[i] == Align.MAX:
align_offset.append(-bbox.maxs[i])
align_offset.append(-bbox.max.to_tuple()[i])
text_flat = text_flat.translate(Vector(*align_offset))
if text_path is not None:
@ -5390,7 +5373,6 @@ class Face(Shape):
return BRep_Tool.Surface_s(self.wrapped)
def _uv_bounds(self) -> Tuple[float, float, float, float]:
return BRepTools.UVBounds_s(self.wrapped)
def __neg__(self) -> Face:
@ -5954,9 +5936,7 @@ class Face(Shape):
ShapeList[Face]: Face(s) projected on target object ordered by distance
"""
max_dimension = (
Compound.make_compound([self, target_object])
.bounding_box()
.diagonal_length()
Compound.make_compound([self, target_object]).bounding_box().diagonal
)
face_extruded = Solid.extrude_linear(
self, Vector(direction) * max_dimension, taper=taper
@ -6457,9 +6437,7 @@ class Solid(Shape, Mixin3D):
direction = Vector(direction)
max_dimension = (
Compound.make_compound([section, target_object])
.bounding_box()
.diagonal_length()
Compound.make_compound([section, target_object]).bounding_box().diagonal
)
clipping_direction = (
direction * max_dimension
@ -6565,7 +6543,6 @@ class Solid(Shape, Mixin3D):
path: Union[Wire, Edge],
mode: Union[Vector, Wire, Edge],
) -> bool:
rotate = False
if isinstance(mode, Vector):
@ -7363,7 +7340,7 @@ class SVG:
[(0, 0, 0), (-axes_scale / 20, axes_scale / 30, 0)],
[(-1, 0, 0), (-1, 1.5, 0)],
)
arrow = arrow_arc.fuse(arrow_arc.copy().mirror(Plane.XZ))
arrow = arrow_arc.fuse(copy.copy(arrow_arc).mirror(Plane.XZ))
x_label = (
Compound.make_2d_text(
"X", fontsize=axes_scale / 4, align=(Align.MIN, Align.CENTER)
@ -7543,14 +7520,14 @@ class SVG:
# width pixels for x, height pixels for y
if defaults["pixel_scale"]:
unit_scale = defaults["pixel_scale"]
width = int(unit_scale * b_box.xlen + 2 * defaults["margin_left"])
height = int(unit_scale * b_box.ylen + 2 * defaults["margin_left"])
width = int(unit_scale * b_box.size.X + 2 * defaults["margin_left"])
height = int(unit_scale * b_box.size.Y + 2 * defaults["margin_left"])
else:
unit_scale = min(width / b_box.xlen * 0.75, height / b_box.ylen * 0.75)
unit_scale = min(width / b_box.size.X * 0.75, height / b_box.size.Y * 0.75)
# compute amount to translate-- move the top left into view
(x_translate, y_translate) = (
(0 - b_box.xmin) + margin_left / unit_scale,
(0 - b_box.ymax) - margin_top / unit_scale,
(0 - b_box.min.X) + margin_left / unit_scale,
(0 - b_box.max.Y) - margin_top / unit_scale,
)
# If the user did not specify a stroke width, calculate it based on the unit scale
@ -7700,13 +7677,13 @@ class Joint(ABC):
# pylint doesn't see this as an abstract method and warns about different arguments in
# derived classes
@abstractmethod
def connect_to(self, other: Joint, *args, **kwargs):
def connect_to(self, other: Joint, *args, **kwargs): # pragma: no cover
"""Connect Joint self by repositioning other"""
return NotImplementedError
@property
@abstractmethod
def symbol(self) -> Compound:
def symbol(self) -> Compound: # pragma: no cover
"""A CAD object positioned in global space to illustrate the joint"""
return NotImplementedError
@ -7725,7 +7702,7 @@ class RigidJoint(Joint):
@property
def symbol(self) -> Compound:
"""A CAD symbol (XYZ indicator) as bound to part"""
size = self.parent.bounding_box().diagonal_length() / 12
size = self.parent.bounding_box().diagonal / 12
return SVG.axes(axes_scale=size).locate(
self.parent.location * self.relative_location
)
@ -7777,7 +7754,7 @@ class RevoluteJoint(Joint):
@property
def symbol(self) -> Compound:
"""A CAD symbol representing the axis of rotation as bound to part"""
radius = self.parent.bounding_box().diagonal_length() / 30
radius = self.parent.bounding_box().diagonal / 30
return Compound.make_compound(
[
@ -7826,7 +7803,7 @@ class RevoluteJoint(Joint):
raise TypeError(f"other must of type RigidJoint not {type(other)}")
angle = self.angular_range[0] if angle is None else angle
if not self.angular_range[0] <= angle <= self.angular_range[1]:
if angle < self.angular_range[0] or angle > self.angular_range[1]:
raise ValueError(f"angle ({angle}) must in range of {self.angular_range}")
self.angle = angle
# Avoid strange rotations when angle is zero by using 360 instead
@ -7969,7 +7946,16 @@ class LinearJoint(Joint):
* rotation
)
other.parent.locate(self.parent.location * joint_relative_position)
if isinstance(other, RevoluteJoint):
other_relative_location = Location(other.relative_axis.position)
else:
other_relative_location = other.relative_location
other.parent.locate(
self.parent.location
* joint_relative_position
* other_relative_location.inverse()
)
self.connected_to = other
@ -8006,10 +7992,10 @@ class CylindricalJoint(Joint):
]
).move(self.parent.location * self.relative_axis.to_location())
@property
def axis_location(self) -> Location:
"""Current global location of joint axis"""
return self.parent.location * self.relative_axis.to_location()
# @property
# def axis_location(self) -> Location:
# """Current global location of joint axis"""
# return self.parent.location * self.relative_axis.to_location()
def __init__(
self,
@ -8080,7 +8066,10 @@ class CylindricalJoint(Joint):
)
)
other.parent.locate(
self.parent.location * joint_relative_position * joint_rotation
self.parent.location
* joint_relative_position
* joint_rotation
* other.relative_location.inverse()
)
self.connected_to = other
@ -8103,7 +8092,7 @@ class BallJoint(Joint):
@property
def symbol(self) -> Compound:
"""A CAD symbol representing joint as bound to part"""
radius = self.parent.bounding_box().diagonal_length() / 30
radius = self.parent.bounding_box().diagonal / 30
circle_x = Edge.make_circle(radius, self.angle_reference)
circle_y = Edge.make_circle(radius, self.angle_reference.rotated((90, 0, 0)))
circle_z = Edge.make_circle(radius, self.angle_reference.rotated((0, 90, 0)))

View file

@ -116,10 +116,10 @@ class BuildLineTests(unittest.TestCase):
with BuildLine() as el:
EllipticalCenterArc((0, 0), 10, 5, 0, 180)
bbox = el.line.bounding_box()
self.assertGreaterEqual(bbox.xmin, -10)
self.assertGreaterEqual(bbox.ymin, 0)
self.assertLessEqual(bbox.xmax, 10)
self.assertLessEqual(bbox.ymax, 5)
self.assertGreaterEqual(bbox.min.X, -10)
self.assertGreaterEqual(bbox.min.Y, 0)
self.assertLessEqual(bbox.max.X, 10)
self.assertLessEqual(bbox.max.Y, 5)
def test_jern_arc(self):
with BuildLine() as jern:

View file

@ -45,12 +45,12 @@ class TestAlign(unittest.TestCase):
with BuildPart() as max:
Box(1, 1, 1, align=(Align.MIN, Align.CENTER, Align.MAX))
bbox = max.part.bounding_box()
self.assertGreaterEqual(bbox.xmin, 0)
self.assertLessEqual(bbox.xmax, 1)
self.assertGreaterEqual(bbox.ymin, -0.5)
self.assertLessEqual(bbox.ymax, 0.5)
self.assertGreaterEqual(bbox.zmin, -1)
self.assertLessEqual(bbox.zmax, 0)
self.assertGreaterEqual(bbox.min.X, 0)
self.assertLessEqual(bbox.max.X, 1)
self.assertGreaterEqual(bbox.min.Y, -0.5)
self.assertLessEqual(bbox.max.Y, 0.5)
self.assertGreaterEqual(bbox.min.Z, -1)
self.assertLessEqual(bbox.max.Z, 0)
class TestBuildPart(unittest.TestCase):
@ -160,6 +160,13 @@ class TestBuildPart(unittest.TestCase):
5,
)
def test_part_transfer_on_exit(self):
with BuildPart(Plane.XY) as test:
Box(1, 1, 1)
with BuildPart(Plane.XY.offset(1)):
Box(1, 1, 1)
self.assertAlmostEqual(test.part.volume, 2, 5)
class TestBuildPartExceptions(unittest.TestCase):
"""Test exception handling"""

View file

@ -44,10 +44,10 @@ class TestAlign(unittest.TestCase):
with BuildSketch() as align:
Rectangle(1, 1, align=(Align.MIN, Align.MAX))
bbox = align.sketch.bounding_box()
self.assertGreaterEqual(bbox.xmin, 0)
self.assertLessEqual(bbox.xmax, 1)
self.assertGreaterEqual(bbox.ymin, -1)
self.assertLessEqual(bbox.ymax, 0)
self.assertGreaterEqual(bbox.min.X, 0)
self.assertLessEqual(bbox.max.X, 1)
self.assertGreaterEqual(bbox.min.Y, -1)
self.assertLessEqual(bbox.max.Y, 0)
class TestBuildSketch(unittest.TestCase):
@ -213,10 +213,10 @@ class TestBuildSketchObjects(unittest.TestCase):
with BuildSketch() as align:
RegularPolygon(2, 5, align=(Align.MIN, Align.MAX))
bbox = align.sketch.bounding_box()
self.assertGreaterEqual(bbox.xmin, 0)
self.assertLessEqual(bbox.xmax, 4)
self.assertGreaterEqual(bbox.ymin, -4)
self.assertLessEqual(bbox.ymax, 1e-5)
self.assertGreaterEqual(bbox.min.X, 0)
self.assertLessEqual(bbox.max.X, 4)
self.assertGreaterEqual(bbox.min.Y, -4)
self.assertLessEqual(bbox.max.Y, 1e-5)
with BuildSketch() as align:
RegularPolygon(2, 5, align=None)

View file

@ -126,20 +126,20 @@ class TestBoundBox(unittest.TestCase):
bb1 = v.bounding_box().add(v2.bounding_box())
# OCC uses some approximations
self.assertAlmostEqual(bb1.xlen, 1.0, 1)
self.assertAlmostEqual(bb1.size.X, 1.0, 1)
# Test adding to an existing bounding box
v0 = Vertex(0, 0, 0)
bb2 = v0.bounding_box().add(v.bounding_box())
bb3 = bb1.add(bb2)
self.assertTupleAlmostEquals((2, 2, 2), (bb3.xlen, bb3.ylen, bb3.zlen), 7)
self.assertTupleAlmostEquals((2, 2, 2), (bb3.size.X, bb3.size.Y, bb3.size.Z), 7)
bb3 = bb2.add((3, 3, 3))
self.assertTupleAlmostEquals((3, 3, 3), (bb3.xlen, bb3.ylen, bb3.zlen), 7)
self.assertTupleAlmostEquals((3, 3, 3), (bb3.size.X, bb3.size.Y, bb3.size.Z), 7)
bb3 = bb2.add(Vector(3, 3, 3))
self.assertTupleAlmostEquals((3, 3, 3), (bb3.xlen, bb3.ylen, bb3.zlen), 7)
self.assertTupleAlmostEquals((3, 3, 3), (bb3.size.X, bb3.size.Y, bb3.size.Z), 7)
# Test 2D bounding boxes
bb1 = Vertex(1, 1, 0).bounding_box().add(Vertex(2, 2, 0).bounding_box())
@ -154,7 +154,7 @@ class TestBoundBox(unittest.TestCase):
# Test creation of a bounding box from a shape - note the low accuracy comparison
# as the box is a little larger than the shape
bb1 = BoundBox._from_topo_ds(Solid.make_cylinder(1, 1).wrapped, optimal=False)
self.assertTupleAlmostEquals((2, 2, 1), (bb1.xlen, bb1.ylen, bb1.zlen), 1)
self.assertTupleAlmostEquals((2, 2, 1), (bb1.size.X, bb1.size.Y, bb1.size.Z), 1)
bb2 = BoundBox._from_topo_ds(
Solid.make_cylinder(0.5, 0.5).translate((0, 0, 0.1)).wrapped, optimal=False
@ -176,12 +176,10 @@ class TestBoundBox(unittest.TestCase):
class TestCadObjects(unittest.TestCase):
def _make_circle(self):
circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp.DZ_s()), 2.0)
return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge())
def _make_ellipse(self):
ellipse = gp_Elips(gp_Ax2(gp_Pnt(1, 2, 3), gp.DZ_s()), 4.0, 2.0)
return Shape.cast(BRepBuilderAPI_MakeEdge(ellipse).Edge())
@ -348,7 +346,6 @@ class TestCadObjects(unittest.TestCase):
self.assertEqual(2, len(e.vertices()))
def test_edge_wrapper_radius(self):
# get a radius from a simple circle
e0 = Edge.make_circle(2.4)
self.assertAlmostEqual(e0.radius, 2.4)
@ -427,7 +424,7 @@ class TestCompound(unittest.TestCase):
text = Compound.make_2d_text("test", 10, text_path=arc)
self.assertEqual(len(text.faces()), 4)
text = Compound.make_2d_text(
"test", 10, align=(Align.MAX,Align.MAX), text_path=arc
"test", 10, align=(Align.MAX, Align.MAX), text_path=arc
)
self.assertEqual(len(text.faces()), 4)
@ -552,9 +549,261 @@ class TestFunctions(unittest.TestCase):
self.assertAlmostEqual(wires[1].length, 6, 5)
class TestJoints(unittest.TestCase):
def test_rigid_joint(self):
base = Solid.make_box(1, 1, 1)
j1 = RigidJoint("top", base, Location(Vector(0.5, 0.5, 1)))
fixed_top = Solid.make_box(1, 1, 1)
j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.5, 0)))
j1.connect_to(j2)
bbox = fixed_top.bounding_box()
self.assertTupleAlmostEquals(bbox.min.to_tuple(), (0, 0, 1), 5)
self.assertTupleAlmostEquals(bbox.max.to_tuple(), (1, 1, 2), 5)
self.assertTupleAlmostEquals(
j2.symbol.location.position.to_tuple(), (0.5, 0.5, 1), 6
)
self.assertTupleAlmostEquals(
j2.symbol.location.orientation.to_tuple(), (0, 0, 0), 6
)
def test_revolute_joint_with_angle_reference(self):
revolute_base = Solid.make_cylinder(1, 1)
j1 = RevoluteJoint(
label="top",
to_part=revolute_base,
axis=Axis((0, 0, 1), (0, 0, 1)),
angle_reference=(1, 0, 0),
angular_range=(0, 180),
)
fixed_top = Solid.make_box(1, 0.5, 1)
j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.25, 0)))
j1.connect_to(j2, 90)
bbox = fixed_top.bounding_box()
self.assertTupleAlmostEquals(bbox.min.to_tuple(), (-0.25, -0.5, 1), 5)
self.assertTupleAlmostEquals(bbox.max.to_tuple(), (0.25, 0.5, 2), 5)
self.assertTupleAlmostEquals(
j2.symbol.location.position.to_tuple(), (0, 0, 1), 6
)
self.assertTupleAlmostEquals(
j2.symbol.location.orientation.to_tuple(), (0, 0, 90), 6
)
self.assertEqual(len(j1.symbol.edges()), 2)
def test_revolute_joint_without_angle_reference(self):
revolute_base = Solid.make_cylinder(1, 1)
j1 = RevoluteJoint(
label="top",
to_part=revolute_base,
axis=Axis((0, 0, 1), (0, 0, 1)),
)
self.assertTupleAlmostEquals(j1.angle_reference.to_tuple(), (1, 0, 0), 5)
def test_revolute_joint_error_bad_angle_reference(self):
"""Test that the angle_reference must be normal to the axis"""
revolute_base = Solid.make_cylinder(1, 1)
with self.assertRaises(ValueError):
RevoluteJoint(
"top",
revolute_base,
axis=Axis((0, 0, 1), (0, 0, 1)),
angle_reference=(1, 0, 1),
)
def test_revolute_joint_error_bad_angle(self):
"""Test that the joint angle is within bounds"""
revolute_base = Solid.make_cylinder(1, 1)
j1 = RevoluteJoint("top", revolute_base, Axis.Z, angular_range=(0, 180))
fixed_top = Solid.make_box(1, 0.5, 1)
j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.25, 0)))
with self.assertRaises(ValueError):
j1.connect_to(j2, 270)
def test_revolute_joint_error_bad_joint_type(self):
"""Test that the joint angle is within bounds"""
revolute_base = Solid.make_cylinder(1, 1)
j1 = RevoluteJoint("top", revolute_base, Axis.Z, (0, 180))
fixed_top = Solid.make_box(1, 0.5, 1)
j2 = RevoluteJoint("bottom", fixed_top, Axis.Z, (0, 180))
with self.assertRaises(TypeError):
j1.connect_to(j2, 0)
def test_linear_rigid_joint(self):
base = Solid.make_box(1, 1, 1)
j1 = LinearJoint(
"top", to_part=base, axis=Axis((0, 0.5, 1), (1, 0, 0)), linear_range=(0, 1)
)
fixed_top = Solid.make_box(1, 1, 1)
j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.5, 0)))
j1.connect_to(j2, 0.25)
bbox = fixed_top.bounding_box()
self.assertTupleAlmostEquals(bbox.min.to_tuple(), (-0.25, 0, 1), 5)
self.assertTupleAlmostEquals(bbox.max.to_tuple(), (0.75, 1, 2), 5)
self.assertTupleAlmostEquals(
j2.symbol.location.position.to_tuple(), (0.25, 0.5, 1), 6
)
self.assertTupleAlmostEquals(
j2.symbol.location.orientation.to_tuple(), (0, 0, 0), 6
)
def test_linear_revolute_joint(self):
linear_base = Solid.make_box(1, 1, 1)
j1 = LinearJoint(
label="top",
to_part=linear_base,
axis=Axis((0, 0.5, 1), (1, 0, 0)),
linear_range=(0, 1),
)
revolute_top = Solid.make_box(1, 0.5, 1).locate(Location((-0.5, -0.25, 0)))
j2 = RevoluteJoint(
label="top",
to_part=revolute_top,
axis=Axis((0, 0, 0), (0, 0, 1)),
angle_reference=(1, 0, 0),
angular_range=(0, 180),
)
j1.connect_to(j2, position=0.25, angle=90)
bbox = revolute_top.bounding_box()
self.assertTupleAlmostEquals(bbox.min.to_tuple(), (0, 0, 1), 5)
self.assertTupleAlmostEquals(bbox.max.to_tuple(), (0.5, 1, 2), 5)
self.assertTupleAlmostEquals(
j2.symbol.location.position.to_tuple(), (0.25, 0.5, 1), 6
)
self.assertTupleAlmostEquals(
j2.symbol.location.orientation.to_tuple(), (0, 0, 90), 6
)
self.assertEqual(len(j1.symbol.edges()), 2)
# Test invalid position
with self.assertRaises(ValueError):
j1.connect_to(j2, position=5, angle=90)
# Test invalid angle
with self.assertRaises(ValueError):
j1.connect_to(j2, position=0.5, angle=270)
# Test invalid joint
with self.assertRaises(TypeError):
j1.connect_to(Solid.make_box(1, 1, 1), position=0.5, angle=90)
def test_cylindrical_joint(self):
cylindrical_base = (
Solid.make_box(1, 1, 1)
.locate(Location((-0.5, -0.5, 0)))
.cut(Solid.make_cylinder(0.3, 1))
)
j1 = CylindricalJoint(
"base",
cylindrical_base,
Axis((0, 0, 1), (0, 0, -1)),
angle_reference=(1, 0, 0),
linear_range=(0, 1),
angular_range=(0, 90),
)
dowel = Solid.make_cylinder(0.3, 1).cut(
Solid.make_box(1, 1, 1).locate(Location((-0.5, 0, 0)))
)
j2 = RigidJoint("bottom", dowel, Location((0, 0, 0), (0, 0, 0)))
j1.connect_to(j2, 0.25, 90)
dowel_bbox = dowel.bounding_box()
self.assertTupleAlmostEquals(dowel_bbox.min.to_tuple(), (0, -0.3, -0.25), 5)
self.assertTupleAlmostEquals(dowel_bbox.max.to_tuple(), (0.3, 0.3, 0.75), 5)
self.assertTupleAlmostEquals(
j1.symbol.location.position.to_tuple(), (0, 0, 1), 6
)
self.assertTupleAlmostEquals(
j1.symbol.location.orientation.to_tuple(), (-180, 0, -180), 6
)
self.assertEqual(len(j1.symbol.edges()), 2)
# Test invalid position
with self.assertRaises(ValueError):
j1.connect_to(j2, position=5, angle=90)
# Test invalid angle
with self.assertRaises(ValueError):
j1.connect_to(j2, position=0.5, angle=270)
# Test invalid joint
with self.assertRaises(TypeError):
j1.connect_to(Solid.make_box(1, 1, 1), position=0.5, angle=90)
def test_cylindrical_joint_error_bad_angle_reference(self):
"""Test that the angle_reference must be normal to the axis"""
with self.assertRaises(ValueError):
CylindricalJoint(
"base",
Solid.make_box(1, 1, 1),
Axis((0, 0, 1), (0, 0, -1)),
angle_reference=(1, 0, 1),
linear_range=(0, 1),
angular_range=(0, 90),
)
def test_cylindrical_joint_error_bad_position_and_angle(self):
"""Test that the joint angle is within bounds"""
j1 = CylindricalJoint(
"base",
Solid.make_box(1, 1, 1),
Axis((0, 0, 1), (0, 0, -1)),
linear_range=(0, 1),
angular_range=(0, 90),
)
j2 = RigidJoint("bottom", Solid.make_cylinder(1, 1), Location((0.5, 0.25, 0)))
with self.assertRaises(ValueError):
j1.connect_to(j2, position=0.5, angle=270)
with self.assertRaises(ValueError):
j1.connect_to(j2, position=4, angle=30)
def test_ball_joint(self):
socket_base = Solid.make_box(1, 1, 1).cut(
Solid.make_sphere(0.3, Plane((0.5, 0.5, 1)))
)
j1 = BallJoint(
"socket",
socket_base,
Location((0.5, 0.5, 1)),
angular_range=((-45, 45), (-45, 45), (0, 360)),
)
ball_rod = Solid.make_cylinder(0.15, 2).fuse(
Solid.make_sphere(0.3).locate(Location((0, 0, 2)))
)
j2 = RigidJoint("ball", ball_rod, Location((0, 0, 2), (180, 0, 0)))
j1.connect_to(j2, (45, 45, 0))
self.assertTupleAlmostEquals(
ball_rod.faces()
.filter_by(GeomType.PLANE)[0]
.center(CenterOf.GEOMETRY)
.to_tuple(),
(1.914213562373095, -0.5, 2),
5,
)
self.assertTupleAlmostEquals(
j1.symbol.location.position.to_tuple(), (0.5, 0.5, 1), 6
)
self.assertTupleAlmostEquals(
j1.symbol.location.orientation.to_tuple(), (0, 0, 0), 6
)
with self.assertRaises(ValueError):
j1.connect_to(j2, (90, 45, 0))
# Test invalid joint
with self.assertRaises(TypeError):
j1.connect_to(Solid.make_box(1, 1, 1), (0, 0, 0))
class TestLocation(unittest.TestCase):
def test_location(self):
loc0 = Location()
T = loc0.wrapped.Transformation().TranslationPart()
self.assertTupleAlmostEquals((T.X(), T.Y(), T.Z()), (0, 0, 0), 6)
@ -1270,7 +1519,6 @@ class TestPlane(unittest.TestCase):
class ProjectionTests(unittest.TestCase):
def test_flat_projection(self):
sphere = Solid.make_sphere(50)
projection_direction = Vector(0, -1, 0)
planar_text_faces = (
@ -1310,7 +1558,6 @@ class ProjectionTests(unittest.TestCase):
# self.assertEqual(len(projected_faces), 1)
def test_text_projection(self):
sphere = Solid.make_sphere(50)
arch_path = (
sphere.cut(
@ -1353,14 +1600,14 @@ class TestShape(unittest.TestCase):
def test_mirror(self):
box_bb = Solid.make_box(1, 1, 1).mirror(Plane.XZ).bounding_box()
self.assertAlmostEqual(box_bb.xmin, 0, 5)
self.assertAlmostEqual(box_bb.xmax, 1, 5)
self.assertAlmostEqual(box_bb.ymin, -1, 5)
self.assertAlmostEqual(box_bb.ymax, 0, 5)
self.assertAlmostEqual(box_bb.min.X, 0, 5)
self.assertAlmostEqual(box_bb.max.X, 1, 5)
self.assertAlmostEqual(box_bb.min.Y, -1, 5)
self.assertAlmostEqual(box_bb.max.Y, 0, 5)
box_bb = Solid.make_box(1, 1, 1).mirror().bounding_box()
self.assertAlmostEqual(box_bb.zmin, -1, 5)
self.assertAlmostEqual(box_bb.zmax, 0, 5)
self.assertAlmostEqual(box_bb.min.Z, -1, 5)
self.assertAlmostEqual(box_bb.max.Z, 0, 5)
def test_compute_mass(self):
with self.assertRaises(NotImplementedError):
@ -1451,12 +1698,12 @@ class TestShape(unittest.TestCase):
def test_locate_bb(self):
bounding_box = Solid.make_cone(1, 2, 1).bounding_box()
relocated_bounding_box = Plane.XZ.from_local_coords(bounding_box)
self.assertAlmostEqual(relocated_bounding_box.xmin, -2, 5)
self.assertAlmostEqual(relocated_bounding_box.xmax, 2, 5)
self.assertAlmostEqual(relocated_bounding_box.ymin, 0, 5)
self.assertAlmostEqual(relocated_bounding_box.ymax, -1, 5)
self.assertAlmostEqual(relocated_bounding_box.zmin, -2, 5)
self.assertAlmostEqual(relocated_bounding_box.zmax, 2, 5)
self.assertAlmostEqual(relocated_bounding_box.min.X, -2, 5)
self.assertAlmostEqual(relocated_bounding_box.max.X, 2, 5)
self.assertAlmostEqual(relocated_bounding_box.min.Y, 0, 5)
self.assertAlmostEqual(relocated_bounding_box.max.Y, -1, 5)
self.assertAlmostEqual(relocated_bounding_box.min.Z, -2, 5)
self.assertAlmostEqual(relocated_bounding_box.max.Z, 2, 5)
def test_is_equal(self):
box = Solid.make_box(1, 1, 1)