mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Changed bounding box to Vectors, Added Joints tests
This commit is contained in:
parent
bf3aaefe10
commit
4500d333de
12 changed files with 408 additions and 156 deletions
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue