mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Improved algebra +,-,& operators Issue #752
This commit is contained in:
parent
9fb163cb64
commit
0b4b2b2b54
2 changed files with 205 additions and 50 deletions
|
|
@ -1699,64 +1699,94 @@ class Shape(NodeMixin):
|
|||
|
||||
def __add__(self, other: Union[list[Shape], Shape]) -> Self:
|
||||
"""fuse shape to self operator +"""
|
||||
others = other if isinstance(other, (list, tuple)) else [other]
|
||||
# Convert `other` to list of base objects and filter out None values
|
||||
summands = [
|
||||
shape
|
||||
for o in (other if isinstance(other, (list, tuple)) else [other])
|
||||
if o is not None
|
||||
for shape in (o.first_level_shapes() if isinstance(o, Compound) else [o])
|
||||
]
|
||||
# If there is nothing to add return the original object
|
||||
if not summands:
|
||||
return self
|
||||
|
||||
if not all([type(other)._dim == type(self)._dim for other in others]):
|
||||
# Check that all dimensions are the same
|
||||
addend_dim = self._dim
|
||||
if addend_dim is None:
|
||||
raise ValueError("Dimensions of objects to add to are inconsistent")
|
||||
|
||||
if not all(summand._dim == addend_dim for summand in summands):
|
||||
raise ValueError("Only shapes with the same dimension can be added")
|
||||
|
||||
if self.wrapped is None:
|
||||
if len(others) == 1:
|
||||
new_shape = others[0]
|
||||
if self.wrapped is None: # an empty object
|
||||
if len(summands) == 1:
|
||||
sum_shape = summands[0]
|
||||
else:
|
||||
new_shape = others[0].fuse(*others[1:])
|
||||
elif isinstance(other, Shape) and other.wrapped is None:
|
||||
new_shape = self
|
||||
sum_shape = summands[0].fuse(*summands[1:])
|
||||
else:
|
||||
new_shape = self.fuse(*others)
|
||||
sum_shape = self.fuse(*summands)
|
||||
|
||||
if SkipClean.clean:
|
||||
new_shape = new_shape.clean()
|
||||
|
||||
if self._dim == 3:
|
||||
new_shape = Part(new_shape.wrapped)
|
||||
elif self._dim == 2:
|
||||
new_shape = Sketch(new_shape.wrapped)
|
||||
elif self._dim == 1:
|
||||
new_shape = Curve(Compound(new_shape.edges()).wrapped)
|
||||
|
||||
return new_shape
|
||||
|
||||
def __sub__(self, other: Shape) -> Self:
|
||||
"""cut shape from self operator -"""
|
||||
others = other if isinstance(other, (list, tuple)) else [other]
|
||||
|
||||
for _other in others:
|
||||
if type(_other)._dim < type(self)._dim:
|
||||
raise ValueError(
|
||||
f"Only shapes with equal or greater dimension can be subtracted: "
|
||||
f"not {type(self).__name__} ({type(self)._dim}D) and "
|
||||
f"{type(_other).__name__} ({type(_other)._dim}D)"
|
||||
# Simplify Compounds if possible
|
||||
sum_shape = (
|
||||
sum_shape.unwrap(fully=True)
|
||||
if isinstance(sum_shape, Compound)
|
||||
else sum_shape
|
||||
)
|
||||
|
||||
new_shape = None
|
||||
if SkipClean.clean:
|
||||
sum_shape = sum_shape.clean()
|
||||
|
||||
# To allow the @, % and ^ operators to work 1D objects must be type Curve
|
||||
if addend_dim == 1:
|
||||
sum_shape = Curve(Compound(sum_shape.edges()).wrapped)
|
||||
|
||||
return sum_shape
|
||||
|
||||
def __sub__(self, other: Union[Shape, Iterable[Shape]]) -> Self:
|
||||
"""cut shape from self operator -"""
|
||||
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Cannot subtract shape from empty compound")
|
||||
if isinstance(other, Shape) and other.wrapped is None:
|
||||
new_shape = self
|
||||
else:
|
||||
new_shape = self.cut(*others)
|
||||
|
||||
if new_shape is not None and SkipClean.clean:
|
||||
new_shape = new_shape.clean()
|
||||
# Convert `other` to list of base objects and filter out None values
|
||||
subtrahends = [
|
||||
shape
|
||||
for o in (other if isinstance(other, (list, tuple)) else [other])
|
||||
if o is not None
|
||||
for shape in (o.first_level_shapes() if isinstance(o, Compound) else [o])
|
||||
]
|
||||
# If there is nothing to subtract return the original object
|
||||
if not subtrahends:
|
||||
return self
|
||||
|
||||
if self._dim == 3:
|
||||
new_shape = Part(new_shape.wrapped)
|
||||
elif self._dim == 2:
|
||||
new_shape = Sketch(new_shape.wrapped)
|
||||
elif self._dim == 1:
|
||||
new_shape = Curve(Compound(new_shape.edges()).wrapped)
|
||||
# Check that all dimensions are the same
|
||||
minuend_dim = self._dim
|
||||
if minuend_dim is None:
|
||||
raise ValueError("Dimensions of objects to subtract from are inconsistent")
|
||||
|
||||
return new_shape
|
||||
# Check that the operation is valid
|
||||
subtrahend_dims = [s._dim for s in subtrahends]
|
||||
if any(d < minuend_dim for d in subtrahend_dims):
|
||||
raise ValueError(
|
||||
f"Only shapes with equal or greater dimension can be subtracted: "
|
||||
f"not {type(self).__name__} ({minuend_dim}D) and "
|
||||
f"{type(other).__name__} ({min(subtrahend_dims)}D)"
|
||||
)
|
||||
|
||||
# Do the actual cut operation
|
||||
difference = self.cut(*subtrahends)
|
||||
|
||||
# Simplify Compounds if possible
|
||||
difference = (
|
||||
difference.unwrap(fully=True)
|
||||
if isinstance(difference, Compound)
|
||||
else difference
|
||||
)
|
||||
# To allow the @, % and ^ operators to work 1D objects must be type Curve
|
||||
if minuend_dim == 1:
|
||||
difference = Curve(Compound(difference.edges()).wrapped)
|
||||
|
||||
return difference
|
||||
|
||||
def __and__(self, other: Shape) -> Self:
|
||||
"""intersect shape with self operator &"""
|
||||
|
|
@ -1769,11 +1799,15 @@ class Shape(NodeMixin):
|
|||
if new_shape.wrapped is not None and SkipClean.clean:
|
||||
new_shape = new_shape.clean()
|
||||
|
||||
if self._dim == 3:
|
||||
new_shape = Part(new_shape.wrapped)
|
||||
elif self._dim == 2:
|
||||
new_shape = Sketch(new_shape.wrapped)
|
||||
elif self._dim == 1:
|
||||
# Simplify Compounds if possible
|
||||
new_shape = (
|
||||
new_shape.unwrap(fully=True)
|
||||
if isinstance(new_shape, Compound)
|
||||
else new_shape
|
||||
)
|
||||
|
||||
# To allow the @, % and ^ operators to work 1D objects must be type Curve
|
||||
if self._dim == 1:
|
||||
new_shape = Curve(Compound(new_shape.edges()).wrapped)
|
||||
|
||||
return new_shape
|
||||
|
|
@ -3940,6 +3974,12 @@ class Compound(Mixin3D, Shape):
|
|||
|
||||
_dim = None
|
||||
|
||||
@property
|
||||
def _dim(self) -> Union[int, None]:
|
||||
"""The dimension of the shapes within the Compound - None if inconsistent"""
|
||||
sub_dims = {s._dim for s in self.first_level_shapes()}
|
||||
return sub_dims.pop() if len(sub_dims) == 1 else None
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -4524,6 +4564,8 @@ class Compound(Mixin3D, Shape):
|
|||
Returns:
|
||||
ShapeList[Shape]: Shapes contained within the Compound
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
return ShapeList()
|
||||
if _shapes is None:
|
||||
_shapes = []
|
||||
iterator = TopoDS_Iterator()
|
||||
|
|
@ -4575,18 +4617,30 @@ class Part(Compound):
|
|||
|
||||
_dim = 3
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 3
|
||||
|
||||
|
||||
class Sketch(Compound):
|
||||
"""A Compound containing 2D objects - aka Faces"""
|
||||
|
||||
_dim = 2
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 2
|
||||
|
||||
|
||||
class Curve(Compound):
|
||||
"""A Compound containing 1D objects - aka Edges"""
|
||||
|
||||
_dim = 1
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 1
|
||||
|
||||
def __matmul__(self, position: float) -> Vector:
|
||||
"""Position on curve operator @ - only works if continuous"""
|
||||
return Wire(self.edges()).position_at(position)
|
||||
|
|
@ -4617,6 +4671,10 @@ class Edge(Mixin1D, Shape):
|
|||
|
||||
_dim = 1
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 1
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -5544,6 +5602,10 @@ class Face(Shape):
|
|||
|
||||
_dim = 2
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 2
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -6743,6 +6805,10 @@ class Shell(Shape):
|
|||
|
||||
_dim = 2
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 2
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -6922,6 +6988,10 @@ class Solid(Mixin3D, Shape):
|
|||
|
||||
_dim = 3
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 3
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -7713,6 +7783,10 @@ class Vertex(Shape):
|
|||
|
||||
_dim = 0
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 0
|
||||
|
||||
@overload
|
||||
def __init__(self): # pragma: no cover
|
||||
"""Default Vertext at the origin"""
|
||||
|
|
@ -7894,6 +7968,10 @@ class Wire(Mixin1D, Shape):
|
|||
|
||||
_dim = 1
|
||||
|
||||
@property
|
||||
def _dim(self) -> int:
|
||||
return 1
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -553,12 +553,14 @@ class AlgebraTests(unittest.TestCase):
|
|||
def test_empty_plus_part(self):
|
||||
b = Box(1, 2, 3)
|
||||
r = Part() + b
|
||||
self.assertEqual(b.wrapped, r.wrapped)
|
||||
self.assertAlmostEqual(b.volume, r.volume, 5)
|
||||
self.assertEqual(r._dim, 3)
|
||||
|
||||
def test_part_plus_empty(self):
|
||||
b = Box(1, 2, 3)
|
||||
r = b + Part()
|
||||
self.assertEqual(b.wrapped, r.wrapped)
|
||||
self.assertAlmostEqual(b.volume, r.volume, 5)
|
||||
self.assertEqual(r._dim, 3)
|
||||
|
||||
def test_empty_minus_part(self):
|
||||
b = Box(1, 2, 3)
|
||||
|
|
@ -585,12 +587,14 @@ class AlgebraTests(unittest.TestCase):
|
|||
def test_empty_plus_sketch(self):
|
||||
b = Rectangle(1, 2)
|
||||
r = Sketch() + b
|
||||
self.assertEqual(b.wrapped, r.wrapped)
|
||||
self.assertAlmostEqual(b.area, r.area, 5)
|
||||
self.assertEqual(r._dim, 2)
|
||||
|
||||
def test_sketch_plus_empty(self):
|
||||
b = Rectangle(1, 2)
|
||||
r = b + Sketch()
|
||||
self.assertEqual(b.wrapped, r.wrapped)
|
||||
self.assertAlmostEqual(b.area, r.area, 5)
|
||||
self.assertEqual(r._dim, 2)
|
||||
|
||||
def test_empty_minus_sketch(self):
|
||||
b = Rectangle(1, 2)
|
||||
|
|
@ -659,6 +663,79 @@ class AlgebraTests(unittest.TestCase):
|
|||
with self.assertRaises(ValueError):
|
||||
_ = rectangle - line
|
||||
|
||||
def test_compound_plus(self):
|
||||
addend0 = Box(4, 4, 4)
|
||||
addend1 = Box(1, 1, 8)
|
||||
base_shapes = PolarLocations(15, 20) * addend0
|
||||
addons = PolarLocations(16, 20) * addend1
|
||||
|
||||
p1 = Compound(base_shapes) + Compound(addons)
|
||||
self.assertEqual(p1._dim, 3)
|
||||
self.assertAlmostEqual(p1.volume, 20 * (4**3 + 1 * 1 * 4), 5)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
Compound(children=[addend0] + addend1.faces()) + Box(1, 1, 1)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
Box(1, 1, 1) + Compound(children=addend1.faces())
|
||||
|
||||
def test_compound_minus(self):
|
||||
minuend = Box(4, 4, 4)
|
||||
subtrahend = Box(1, 1, 4)
|
||||
base_shapes = PolarLocations(15, 20) * minuend
|
||||
holes = PolarLocations(17, 20) * subtrahend
|
||||
s1 = Compound(base_shapes) - Compound(holes)
|
||||
self.assertAlmostEqual(s1.volume, 20 * (16 - 0.5) * 4, 5)
|
||||
|
||||
s2 = minuend - subtrahend
|
||||
self.assertEqual(s2._dim, 3)
|
||||
self.assertAlmostEqual(s2.volume, (16 - 1) * 4, 5)
|
||||
|
||||
s3 = minuend.solid() - subtrahend.solid()
|
||||
self.assertEqual(s3._dim, 3)
|
||||
self.assertAlmostEqual(s3.volume, (16 - 1) * 4, 5)
|
||||
|
||||
s4 = Compound(minuend.faces()) - subtrahend
|
||||
self.assertEqual(s4._dim, 2)
|
||||
self.assertAlmostEqual(s4.area, 4 * 16 + 2 * (16 - 1), 5)
|
||||
|
||||
s5 = minuend.shell() - subtrahend
|
||||
self.assertEqual(s5._dim, 2)
|
||||
self.assertAlmostEqual(s5.area, 4 * 16 + 2 * (16 - 1), 5)
|
||||
|
||||
s6 = minuend - None
|
||||
self.assertEqual(s6._dim, 3)
|
||||
self.assertAlmostEqual(s6.volume, 4**3, 5)
|
||||
|
||||
s7 = minuend - Pos((8, 0, 0)) * subtrahend
|
||||
self.assertEqual(s7._dim, 3)
|
||||
self.assertAlmostEqual(s7.volume, 4**3, 5)
|
||||
|
||||
s8 = minuend - [subtrahend, Box(4, 1, 1)]
|
||||
self.assertEqual(s8._dim, 3)
|
||||
self.assertAlmostEqual(s8.volume, (16 - 1) * 4 - 1 * 3, 5)
|
||||
|
||||
s9 = Compound(children=[minuend]) - Compound(children=[subtrahend])
|
||||
self.assertEqual(s9._dim, 3)
|
||||
self.assertAlmostEqual(s9.volume, (16 - 1) * 4, 5)
|
||||
|
||||
s10 = Compound(minuend) - Compound(subtrahend)
|
||||
self.assertEqual(s10._dim, 3)
|
||||
self.assertAlmostEqual(s10.volume, (16 - 1) * 4, 5)
|
||||
|
||||
s11 = minuend.shell() - Compound(children=[subtrahend, Box(4, 1, 1)])
|
||||
self.assertEqual(s11._dim, 2)
|
||||
self.assertAlmostEqual(s11.area, 2 * 16 + 4 * (16 - 1), 5)
|
||||
|
||||
b1 = minuend.shell()
|
||||
s12 = b1 - Compound(children=[subtrahend, b1.faces()[0]])
|
||||
self.assertEqual(s12._dim, 2)
|
||||
self.assertAlmostEqual(s12.area, 3 * 16 + 2 * (16 - 1), 5)
|
||||
self.assertEqual(len(s12.faces()), 5)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
Compound(children=[minuend] + subtrahend.faces()) - Box(1, 1, 1)
|
||||
|
||||
|
||||
class LocationTests(unittest.TestCase):
|
||||
def test_wheel(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue