* Part: Fix mirror() regression with non-identity Placement The makeElementMirror() function incorrectly extracted and pre-multiplied the shape's Location with the mirror transform. Since BRepBuilderAPI_Transform already handles shapes with Location correctly, this resulted in the placement being applied twice, producing incorrect results for shapes with non-identity Placement. Fixes #20834 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Part: Add regression test for mirror() with Placement Adds testTopoShapeMirrorWithPlacement to verify that mirror() produces identical results regardless of whether the shape is positioned via direct coordinates or via Placement. This test would have caught the bug fixed in the previous commit where shapes with non-identity Placement produced incorrect mirror results. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix regression test for mirror with Placement The test was incorrectly trying to create equivalent boxes using different methods that don't actually produce the same geometry. Part.makeBox with a direction vector is not equivalent to setting a Placement with rotation. Fixed to use the correct approach that demonstrates the actual bug: - Method 1: Box with geometry at (0,30,0), identity Placement - Method 2: Box with geometry at origin, moved via Placement Both should produce identical mirror results, which they now do with the makeElementMirror() fix. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Timothy Miller <theosib@zeromark.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
937 lines
36 KiB
Python
937 lines
36 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
import FreeCAD as App
|
|
import Part
|
|
|
|
import unittest
|
|
|
|
|
|
class TopoShapeAssertions:
|
|
|
|
def assertAttrEqual(self, toposhape, attr_value_list, msg=None):
|
|
for attr, value in attr_value_list:
|
|
result = toposhape.__getattribute__(attr) # Look up each attribute by string name
|
|
if result.__str__() != value.__str__():
|
|
if msg == None:
|
|
msg = (f"TopoShape {attr} is incorrect: {result} should be {value}",)
|
|
raise AssertionError(msg)
|
|
|
|
def assertAttrAlmostEqual(self, toposhape, attr_value_list, places=5, msg=None):
|
|
range = 1 / 10**places
|
|
for attr, value in attr_value_list:
|
|
result = toposhape.__getattribute__(attr) # Look up each attribute by string name
|
|
if abs(result - value) > range:
|
|
if msg == None:
|
|
msg = f"TopoShape {attr} is incorrect: {result} should be {value}"
|
|
raise AssertionError(msg)
|
|
|
|
def assertAttrCount(self, toposhape, attr_value_list, msg=None):
|
|
for attr, value in attr_value_list:
|
|
result = toposhape.__getattribute__(attr) # Look up each attribute by string name
|
|
if len(result) != value:
|
|
if msg == None:
|
|
msg = f"TopoShape {attr} is incorrect: {result} should have {value} elements"
|
|
raise AssertionError(msg)
|
|
|
|
def assertKeysInMap(self, map, keys, msg=None):
|
|
for key in keys:
|
|
if not key in map:
|
|
if msg == None:
|
|
msg = f"Key {key} not found in map: {map}"
|
|
raise AssertionError(msg)
|
|
|
|
def assertBounds(self, shape, bounds, msg=None, precision=App.Base.Precision.confusion() * 100):
|
|
shape_bounds = shape.BoundBox
|
|
shape_bounds_max = App.BoundBox(shape_bounds)
|
|
shape_bounds_max.enlarge(precision)
|
|
bounds_max = App.BoundBox(bounds)
|
|
bounds_max.enlarge(precision)
|
|
if not (shape_bounds_max.isInside(bounds) and bounds_max.isInside(shape_bounds)):
|
|
if msg == None:
|
|
msg = f"Bounds {shape_bounds} doesn't match {bounds}"
|
|
raise AssertionError(msg)
|
|
|
|
|
|
class TopoShapeTest(unittest.TestCase, TopoShapeAssertions):
|
|
def setUp(self):
|
|
"""Create a document and some TopoShapes of various types"""
|
|
self.doc = App.newDocument("TopoShape")
|
|
# self.box = Part.makeBox(1, 2, 2)
|
|
# Part.show(self.box, "Box1")
|
|
# self.box2 = Part.makeBox(2, 1, 2)
|
|
# Part.show(self.box2, "Box2")
|
|
# Even on LS3 these boxes have no element maps.
|
|
self.doc.addObject("Part::Box", "Box1")
|
|
self.doc.Box1.Length = 1
|
|
self.doc.Box1.Width = 2
|
|
self.doc.Box1.Height = 2
|
|
self.doc.addObject("Part::Box", "Box2")
|
|
self.doc.Box2.Length = 2
|
|
self.doc.Box2.Width = 1
|
|
self.doc.Box2.Height = 2
|
|
self.doc.addObject("Part::Cylinder", "Cylinder1")
|
|
self.doc.Cylinder1.Radius = 0.5
|
|
self.doc.Cylinder1.Height = 2
|
|
self.doc.Cylinder1.Angle = 360
|
|
self.doc.addObject("Part::Compound", "Compound1")
|
|
self.doc.Compound1.Links = [self.doc.Box1, self.doc.Box2]
|
|
|
|
self.doc.recompute()
|
|
self.box = self.doc.Box1.Shape
|
|
self.box2 = self.doc.Box2.Shape
|
|
|
|
def tearDown(self):
|
|
App.closeDocument("TopoShape")
|
|
|
|
def testTopoShapeBox(self):
|
|
# Arrange our test TopoShape
|
|
box2_toposhape = self.doc.Box2.Shape
|
|
# Arrange list of attributes and values to string match
|
|
attr_value_list = [
|
|
["BoundBox", App.BoundBox(0, 0, 0, 2, 1, 2)],
|
|
["CenterOfGravity", App.Vector(1, 0.5, 1)],
|
|
["CenterOfMass", App.Vector(1, 0.5, 1)],
|
|
["CompSolids", []],
|
|
["Compounds", []],
|
|
[
|
|
"Content",
|
|
"<ElementMap/>\n",
|
|
], # Our element map is empty, or there would be more here.
|
|
["ElementMap", {}],
|
|
["ElementReverseMap", {}],
|
|
["Hasher", None],
|
|
[
|
|
"MatrixOfInertia",
|
|
App.Matrix(1.66667, 0, 0, 0, 0, 2.66667, 0, 0, 0, 0, 1.66667, 0, 0, 0, 0, 1),
|
|
],
|
|
["Module", "Part"],
|
|
["Orientation", "Forward"],
|
|
# ['OuterShell', {}], # Todo: Could verify that a Shell Object is returned
|
|
["Placement", App.Placement()],
|
|
[
|
|
"PrincipalProperties",
|
|
{
|
|
"SymmetryAxis": True,
|
|
"SymmetryPoint": False,
|
|
"Moments": (
|
|
2.666666666666666,
|
|
1.666666666666667,
|
|
1.666666666666667,
|
|
),
|
|
"FirstAxisOfInertia": App.Vector(0.0, 1.0, 0.0),
|
|
"SecondAxisOfInertia": App.Vector(0.0, 0.0, 1.0),
|
|
"ThirdAxisOfInertia": App.Vector(1.0, 0.0, 0.0),
|
|
"RadiusOfGyration": (
|
|
0.816496580927726,
|
|
0.6454972243679029,
|
|
0.6454972243679029,
|
|
),
|
|
},
|
|
],
|
|
["ShapeType", "Solid"],
|
|
[
|
|
"StaticMoments",
|
|
(3.999999999999999, 1.9999999999999996, 3.999999999999999),
|
|
],
|
|
# ['Tag', 0], # Gonna vary, so can't really assert, except maybe != 0?
|
|
["TypeId", "Part::TopoShape"],
|
|
]
|
|
# Assert all the expected values match when converted to strings.
|
|
self.assertAttrEqual(box2_toposhape, attr_value_list)
|
|
|
|
# Arrange list of attributes and values to match within 5 decimal places
|
|
attr_value_list = [
|
|
["Area", 16.0],
|
|
["ElementMapSize", 0],
|
|
# ['ElementMapVersion', 4 ], # Todo: Not until TNP on.
|
|
["Length", 40.0], # Sum of all edges of each face, so some redundancy.
|
|
["Mass", 4.0],
|
|
# ['MemSize', 13824], # Platform variations in this size.
|
|
["Volume", 4.0],
|
|
]
|
|
# Assert all the expected values match
|
|
self.assertAttrAlmostEqual(box2_toposhape, attr_value_list, 5)
|
|
|
|
# Arrange list of attributes to check list lengths
|
|
attr_value_list = [
|
|
["Edges", 12],
|
|
["Faces", 6],
|
|
["Shells", 1],
|
|
["Solids", 1],
|
|
["SubShapes", 1],
|
|
["Vertexes", 8],
|
|
["Wires", 6],
|
|
]
|
|
# Assert all the expected values match
|
|
self.assertAttrCount(box2_toposhape, attr_value_list)
|
|
|
|
def testTopoShapeElementMap(self):
|
|
"""Tests TopoShape elementMap"""
|
|
# Arrange
|
|
# Act No elementMaps exist in base shapes until we perform an operation.
|
|
compound1 = Part.Compound([self.doc.Objects[-1].Shape, self.doc.Objects[-2].Shape])
|
|
self.doc.addObject("Part::Compound", "Compound")
|
|
self.doc.Compound.Links = [
|
|
App.activeDocument().Box1,
|
|
App.activeDocument().Box2,
|
|
]
|
|
self.doc.recompute()
|
|
compound2 = self.doc.Compound.Shape
|
|
# Assert elementMap
|
|
# This flag indicates that ElementMaps are supported under the current C++ build:
|
|
if compound1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
# 52 is 2 cubes of 26 each: 6 Faces, 12 Edges, 8 Vertexes
|
|
# Todo: This should contain something as soon as the Python interface
|
|
# for Part.Compound TNP exists
|
|
# self.assertEqual(len(compound1.ElementMap), 52,
|
|
# "ElementMap is Incorrect: {0}".format(compound1.ElementMap))
|
|
self.assertEqual(
|
|
compound2.ElementMapSize,
|
|
52,
|
|
"ElementMap is Incorrect: {0}".format(compound2.ElementMap),
|
|
)
|
|
# Assert Shape
|
|
self.assertBounds(compound2, App.BoundBox(0, 0, 0, 2, 2, 2))
|
|
|
|
def testPartCommon(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::MultiCommon", "Common")
|
|
self.doc.Common.Shapes = [self.doc.Box1, self.doc.Box2]
|
|
# Act
|
|
self.doc.recompute()
|
|
common1 = self.doc.Common.Shape
|
|
# Assert elementMap
|
|
if common1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertKeysInMap(
|
|
common1.ElementReverseMap,
|
|
[
|
|
"Edge1",
|
|
"Edge2",
|
|
"Edge3",
|
|
"Edge4",
|
|
"Edge5",
|
|
"Edge6",
|
|
"Edge7",
|
|
"Edge8",
|
|
"Edge9",
|
|
"Edge10",
|
|
"Edge11",
|
|
"Edge12",
|
|
"Face1",
|
|
"Face2",
|
|
"Face3",
|
|
"Face4",
|
|
"Face5",
|
|
"Face6",
|
|
"Vertex1",
|
|
"Vertex2",
|
|
"Vertex3",
|
|
"Vertex4",
|
|
"Vertex5",
|
|
"Vertex6",
|
|
"Vertex7",
|
|
"Vertex8",
|
|
],
|
|
)
|
|
# Assert Shape
|
|
self.assertBounds(common1, App.BoundBox(0, 0, 0, 1, 1, 2))
|
|
|
|
def testPartCut(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Cut", "Cut")
|
|
self.doc.Cut.Base = self.doc.Box1
|
|
self.doc.Cut.Tool = self.doc.Box2
|
|
# Act
|
|
self.doc.recompute()
|
|
cut1 = self.doc.Cut.Shape
|
|
# Assert elementMap
|
|
if cut1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertKeysInMap(
|
|
cut1.ElementReverseMap,
|
|
[
|
|
"Edge1",
|
|
"Edge2",
|
|
"Edge3",
|
|
"Edge4",
|
|
"Edge5",
|
|
"Edge6",
|
|
"Edge7",
|
|
"Edge8",
|
|
"Edge9",
|
|
"Edge10",
|
|
"Edge11",
|
|
"Edge12",
|
|
"Face1",
|
|
"Face2",
|
|
"Face3",
|
|
"Face4",
|
|
"Face5",
|
|
"Face6",
|
|
"Vertex1",
|
|
"Vertex2",
|
|
"Vertex3",
|
|
"Vertex4",
|
|
"Vertex5",
|
|
"Vertex6",
|
|
"Vertex7",
|
|
"Vertex8",
|
|
],
|
|
)
|
|
# Assert Shape
|
|
self.assertBounds(cut1, App.BoundBox(0, 1, 0, 1, 2, 2))
|
|
|
|
def testPartFuse(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Fuse", "Fuse")
|
|
self.doc.Fuse.Refine = False
|
|
self.doc.Fuse.Base = self.doc.Box1
|
|
self.doc.Fuse.Tool = self.doc.Box2
|
|
# Act
|
|
self.doc.recompute()
|
|
fuse1 = self.doc.Fuse.Shape
|
|
# Assert elementMap
|
|
if fuse1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(fuse1.ElementMapSize, 58)
|
|
self.doc.Fuse.Refine = True
|
|
self.doc.recompute()
|
|
self.assertEqual(fuse1.ElementMapSize, 58)
|
|
# Shape is an extruded L, with 8 Faces, 12 Vertexes, 18 Edges
|
|
# Assert Shape
|
|
self.assertBounds(fuse1, App.BoundBox(0, 0, 0, 2, 2, 2))
|
|
|
|
def testAppPartMakeCompound(self):
|
|
# This doesn't do element maps.
|
|
# compound1 = Part.Compound([self.doc.Box1.Shape, self.doc.Box2.Shape])
|
|
# Act
|
|
compound1 = Part.makeCompound([self.doc.Box1.Shape, self.doc.Box2.Shape])
|
|
# Assert elementMap
|
|
if compound1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(compound1.ElementMapSize, 52)
|
|
# Assert Shape
|
|
self.assertBounds(compound1, App.BoundBox(0, 0, 0, 2, 2, 2))
|
|
|
|
def testAppPartMakeShell(self):
|
|
# Act
|
|
shell1 = Part.makeShell(self.doc.Box1.Shape.Faces)
|
|
# Assert elementMap
|
|
if shell1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(shell1.ElementMapSize, 26)
|
|
# Assert Shape
|
|
self.assertBounds(shell1, App.BoundBox(0, 0, 0, 1, 2, 2))
|
|
|
|
def testAppPartMakeFace(self):
|
|
# Act
|
|
face1 = Part.makeFace(self.doc.Box1.Shape.Faces[0], "Part::FaceMakerCheese")
|
|
# Assert elementMap
|
|
if face1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(face1.ElementMapSize, 10)
|
|
# Assert Shape
|
|
self.assertBounds(face1, App.BoundBox(0, 0, 0, 0, 2, 2))
|
|
|
|
def testAppPartmakeFilledFace(self):
|
|
face1 = Part.makeFilledFace(self.doc.Box1.Shape.Faces[3].Edges)
|
|
# Assert elementMap
|
|
if face1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(face1.ElementMapSize, 9)
|
|
# Assert Shape
|
|
self.assertBounds(face1, App.BoundBox(-0.05, 2, -0.1, 1.05, 2, 2.1))
|
|
|
|
def testAppPartMakeSolid(self):
|
|
# Act
|
|
solid1 = Part.makeSolid(self.doc.Box1.Shape.Shells[0])
|
|
# Assert elementMap
|
|
if solid1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(solid1.ElementMapSize, 26)
|
|
# Assert Shape
|
|
self.assertBounds(solid1, App.BoundBox(0, 0, 0, 1, 2, 2))
|
|
|
|
def testAppPartMakeRuled(self):
|
|
# Act
|
|
surface1 = Part.makeRuledSurface(*self.doc.Box1.Shape.Edges[3:5])
|
|
# Assert elementMap
|
|
if surface1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(surface1.ElementMapSize, 9)
|
|
# Assert Shape
|
|
self.assertBounds(surface1, App.BoundBox(0, 0, 0, 1, 2, 2))
|
|
|
|
def testAppPartMakeShellFromWires(self):
|
|
# Arrange
|
|
wire1 = self.doc.Box1.Shape.Wires[0] # .copy() Todo: prints 2 gen/mod warn because
|
|
wire2 = self.doc.Box1.Shape.Wires[1] # Todo: copy() isn't TNP yet. Fix when it is.
|
|
# Act
|
|
shell1 = Part.makeShellFromWires([wire1, wire2])
|
|
# Assert elementMap
|
|
if shell1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(shell1.ElementMapSize, 24)
|
|
# Assert Shape
|
|
self.assertBounds(shell1, App.BoundBox(0, 0, 0, 1, 2, 2))
|
|
|
|
def testAppPartMakeSweepSurface(self):
|
|
# Arrange
|
|
circle = Part.makeCircle(5, App.Vector(0, 0, 0))
|
|
path = Part.makeLine(App.Vector(), App.Vector(0, 0, 10))
|
|
Part.show(circle, "Circle") # Trigger the elementMapping
|
|
Part.show(path, "Path") # Trigger the elementMapping
|
|
del circle
|
|
# Act
|
|
surface1 = Part.makeSweepSurface(self.doc.Path.Shape, self.doc.Circle.Shape, 0.001, 0)
|
|
Part.show(surface1, "Sweep")
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if surface1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(surface1.ElementMapSize, 6)
|
|
self.assertBounds(surface1, App.BoundBox(-5, -5, 0, 5, 5, 10), precision=2)
|
|
else:
|
|
# Todo: WHY is the actual sweep different? That's BAD. However, the "New" approach
|
|
# above, which uses BRepOffsetAPI_MakePipe appears to be correct over the older
|
|
# code which uses Geom_Curve. This is done ostensibly because Geom_Curve is so
|
|
# old that it doesn't even support history, which toponaming needs, but also,
|
|
# the result is just wrong: If you look at the resulting shape after Sweeping
|
|
# a circle along a line, you do not get a circular pipe: you get a circular
|
|
# pipe with About a third of it removed. More specifically, an angle of
|
|
# math.radians(math.degrees(360)%180) * 2 appears to have been applied, which
|
|
# looks suspiciously like a substantial bug in OCCT.
|
|
# Assert Shape
|
|
self.assertBounds(surface1, App.BoundBox(-5, -2.72011, 0, 5, 5, 6.28319), precision=2)
|
|
del surface1
|
|
|
|
def testAppPartMakeLoft(self):
|
|
# Act
|
|
solid1 = Part.makeLoft(self.doc.Box1.Shape.Wires[0:2])
|
|
# Assert elementMap
|
|
if solid1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(solid1.ElementMapSize, 24)
|
|
# Assert Shape
|
|
self.assertBounds(solid1, App.BoundBox(0, 0, 0, 1, 2, 2))
|
|
|
|
def testAppPartMakeSplitShape(self):
|
|
# Todo: Refine this test after all TNP code in place to eliminate warnings.
|
|
# Arrange
|
|
edge1 = self.doc.Box1.Shape.Faces[0].Edges[0].translated(App.Vector(0, 0.5, 0))
|
|
face1 = self.doc.Box1.Shape.Faces[0]
|
|
# Act
|
|
solids1 = Part.makeSplitShape(face1, [(edge1, face1)])
|
|
# Assert elementMap
|
|
self.assertEqual(len(solids1), 2)
|
|
self.assertEqual(len(solids1[0]), 1)
|
|
if solids1[0][0].ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(solids1[0][0].ElementMapSize, 9)
|
|
self.assertEqual(solids1[1][0].ElementMapSize, 9)
|
|
# Assert Shape
|
|
self.assertBounds(solids1[0][0], App.BoundBox(0, 0.5, 0, 0, 2, 2))
|
|
self.assertBounds(solids1[1][0], App.BoundBox(0, 0.5, 0, 0, 2, 2))
|
|
|
|
def testTopoShapePyInit(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Compound", "Compound")
|
|
self.doc.Compound.Links = [
|
|
App.activeDocument().Box1,
|
|
App.activeDocument().Box2,
|
|
]
|
|
self.doc.recompute()
|
|
compound = self.doc.Compound.Shape
|
|
# Act
|
|
new_toposhape = Part.Shape(compound)
|
|
new_empty_toposhape = Part.Shape()
|
|
# Assert elementMap
|
|
if compound.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(compound.ElementMapSize, 52)
|
|
self.assertEqual(new_toposhape.ElementMapSize, 52)
|
|
|
|
def testTopoShapeCopy(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Compound", "Compound")
|
|
self.doc.Compound.Links = [
|
|
App.activeDocument().Box1,
|
|
App.activeDocument().Box2,
|
|
]
|
|
self.doc.recompute()
|
|
compound = self.doc.Compound.Shape
|
|
# Act
|
|
compound_copy = compound.copy()
|
|
# Assert elementMap
|
|
if compound.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(compound.ElementMapSize, 52)
|
|
self.assertEqual(compound_copy.ElementMapSize, 52)
|
|
|
|
def testTopoShapeCleaned(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Compound", "Compound")
|
|
self.doc.Compound.Links = [
|
|
App.activeDocument().Box1,
|
|
App.activeDocument().Box2,
|
|
]
|
|
self.doc.recompute()
|
|
compound = self.doc.Compound.Shape
|
|
# Act
|
|
compound_cleaned = compound.cleaned()
|
|
# Assert elementMap
|
|
if compound.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(compound.ElementMapSize, 52)
|
|
self.assertEqual(compound_cleaned.ElementMapSize, 52)
|
|
|
|
def testTopoShapeReplaceShape(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Compound", "Compound")
|
|
self.doc.Compound.Links = [
|
|
App.activeDocument().Box1,
|
|
App.activeDocument().Box2,
|
|
]
|
|
self.doc.recompute()
|
|
compound = self.doc.Compound.Shape
|
|
# Act
|
|
compound_replaced = compound.replaceShape(
|
|
[(App.activeDocument().Box2.Shape, App.activeDocument().Box1.Shape)]
|
|
)
|
|
# Assert elementMap
|
|
if compound.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(compound.ElementMapSize, 52)
|
|
self.assertEqual(compound_replaced.ElementMapSize, 52)
|
|
|
|
def testTopoShapeRemoveShape(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Compound", "Compound")
|
|
self.doc.Compound.Links = [
|
|
App.activeDocument().Box1,
|
|
App.activeDocument().Box2,
|
|
]
|
|
self.doc.recompute()
|
|
compound = self.doc.Compound.Shape
|
|
# Act
|
|
compound_removed = compound.removeShape([App.ActiveDocument.Box2.Shape])
|
|
# Assert elementMap
|
|
if compound.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(compound.ElementMapSize, 52)
|
|
self.assertEqual(compound_removed.ElementMapSize, 52)
|
|
|
|
def testTopoShapeExtrude(self):
|
|
# Arrange
|
|
face = self.doc.Box1.Shape.Faces[0]
|
|
# Act
|
|
extrude = face.extrude(App.Vector(2, 0, 0))
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if extrude.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(extrude.ElementMapSize, 26)
|
|
|
|
def testTopoShapeRevolve(self):
|
|
# Arrange
|
|
face = self.doc.Box1.Shape.Faces[0]
|
|
# Act
|
|
face.revolve(App.Vector(), App.Vector(1, 0, 0), 45)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if face.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(face.ElementMapSize, 9)
|
|
|
|
def testTopoShapeFuse(self):
|
|
# Act
|
|
fused = self.doc.Box1.Shape.fuse(self.doc.Box2.Shape)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if fused.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(fused.ElementMapSize, 58)
|
|
|
|
def testTopoShapeMultiFuse(self):
|
|
# Act
|
|
fused = self.doc.Box1.Shape.multiFuse([self.doc.Box2.Shape])
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if fused.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(fused.ElementMapSize, 58)
|
|
|
|
def testTopoShapeCommon(self):
|
|
# Act
|
|
common = self.doc.Box1.Shape.common(self.doc.Box2.Shape)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if common.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(common.ElementMapSize, 26)
|
|
|
|
def testTopoShapeSection(self):
|
|
# Act
|
|
section = self.doc.Box1.Shape.Faces[0].section(self.doc.Box2.Shape.Faces[3])
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if section.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(section.ElementMapSize, 3)
|
|
|
|
def testTopoShapeSlice(self):
|
|
# Act
|
|
slice = self.doc.Box1.Shape.slice(App.Vector(10, 10, 0), 1)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
self.assertEqual(len(slice), 1)
|
|
if slice[0].ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(slice[0].ElementMapSize, 8)
|
|
|
|
def testTopoShapeSlices(self):
|
|
# Act
|
|
slices = self.doc.Box1.Shape.Faces[0].slices(App.Vector(10, 10, 0), [1, 2])
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if slices.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(slices.ElementMapSize, 6)
|
|
|
|
def testTopoShapeCut(self):
|
|
# Act
|
|
cut = self.doc.Box1.Shape.cut(self.doc.Box2.Shape)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if cut.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(cut.ElementMapSize, 26)
|
|
|
|
def testTopoShapeGeneralFuse(self):
|
|
# Act
|
|
fuse = self.doc.Box1.Shape.generalFuse([self.doc.Box2.Shape])
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
self.assertEqual(len(fuse), 2)
|
|
if fuse[0].ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(fuse[0].ElementMapSize, 60)
|
|
|
|
def testTopoShapeChildShapes(self):
|
|
# Act
|
|
childShapes = self.doc.Box1.Shape.childShapes()
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
self.assertEqual(len(childShapes), 1)
|
|
if childShapes[0].ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(childShapes[0].ElementMapSize, 26)
|
|
|
|
def testTopoShapeMirror(self):
|
|
# Act
|
|
mirror = self.doc.Box1.Shape.mirror(App.Vector(), App.Vector(1, 0, 0))
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if mirror.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(mirror.ElementMapSize, 26)
|
|
|
|
def testTopoShapeMirrorWithPlacement(self):
|
|
"""Test that mirror() produces identical results regardless of how the
|
|
shape is positioned - via direct coordinates or via Placement.
|
|
Regression test for GitHub issue #20834.
|
|
|
|
The bug was: when a shape has a non-identity Location (Placement),
|
|
the mirror result was incorrect because the placement was being
|
|
double-applied in makeElementMirror().
|
|
"""
|
|
# Create two identical boxes at the same visual location using different methods:
|
|
# Method 1: Box geometry positioned directly at (0, 30, 0), identity Placement
|
|
box_direct = Part.makeBox(10, 20, 30, App.Vector(0, 30, 0))
|
|
|
|
# Method 2: Box geometry at origin, then moved via Placement
|
|
box_placed = Part.makeBox(10, 20, 30)
|
|
box_placed.Placement = App.Placement(App.Vector(0, 30, 0), App.Rotation())
|
|
|
|
# Verify both boxes appear at the same location
|
|
self.assertAlmostEqual(box_direct.BoundBox.XMin, box_placed.BoundBox.XMin, places=5)
|
|
self.assertAlmostEqual(box_direct.BoundBox.YMin, box_placed.BoundBox.YMin, places=5)
|
|
self.assertAlmostEqual(box_direct.BoundBox.ZMin, box_placed.BoundBox.ZMin, places=5)
|
|
|
|
# Mirror both across the XZ plane (Y=0)
|
|
# A point (x, y, z) mirrors to (x, -y, z)
|
|
# So box at Y=30..50 should mirror to Y=-50..-30
|
|
mirror_direct = box_direct.mirror(App.Vector(), App.Vector(0, 1, 0))
|
|
mirror_placed = box_placed.mirror(App.Vector(), App.Vector(0, 1, 0))
|
|
|
|
# The mirrored shapes should have identical bounding boxes
|
|
self.assertAlmostEqual(
|
|
mirror_direct.BoundBox.XMin,
|
|
mirror_placed.BoundBox.XMin,
|
|
places=5,
|
|
msg="Mirror with Placement produced different XMin",
|
|
)
|
|
self.assertAlmostEqual(
|
|
mirror_direct.BoundBox.YMin,
|
|
mirror_placed.BoundBox.YMin,
|
|
places=5,
|
|
msg="Mirror with Placement produced different YMin",
|
|
)
|
|
self.assertAlmostEqual(
|
|
mirror_direct.BoundBox.ZMin,
|
|
mirror_placed.BoundBox.ZMin,
|
|
places=5,
|
|
msg="Mirror with Placement produced different ZMin",
|
|
)
|
|
self.assertAlmostEqual(
|
|
mirror_direct.BoundBox.XMax,
|
|
mirror_placed.BoundBox.XMax,
|
|
places=5,
|
|
msg="Mirror with Placement produced different XMax",
|
|
)
|
|
self.assertAlmostEqual(
|
|
mirror_direct.BoundBox.YMax,
|
|
mirror_placed.BoundBox.YMax,
|
|
places=5,
|
|
msg="Mirror with Placement produced different YMax",
|
|
)
|
|
self.assertAlmostEqual(
|
|
mirror_direct.BoundBox.ZMax,
|
|
mirror_placed.BoundBox.ZMax,
|
|
places=5,
|
|
msg="Mirror with Placement produced different ZMax",
|
|
)
|
|
|
|
# Verify the expected mirror result: Y=30..50 mirrors to Y=-50..-30
|
|
self.assertAlmostEqual(mirror_direct.BoundBox.YMin, -50.0, places=5)
|
|
self.assertAlmostEqual(mirror_direct.BoundBox.YMax, -30.0, places=5)
|
|
|
|
def testTopoShapeScale(self):
|
|
# Act
|
|
scale = self.doc.Box1.Shape.scaled(2)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if scale.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(scale.ElementMapSize, 26)
|
|
|
|
def testTopoShapeMakeFillet(self):
|
|
# Act
|
|
fillet = self.doc.Box1.Shape.makeFillet(0.1, self.doc.Box1.Shape.Faces[0].Edges)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if fillet.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(fillet.ElementMapSize, 42)
|
|
|
|
def testTopoShapeMakeChamfer(self):
|
|
# Act
|
|
chamfer = self.doc.Box1.Shape.makeChamfer(0.1, self.doc.Box1.Shape.Faces[0].Edges)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if chamfer.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(chamfer.ElementMapSize, 42)
|
|
|
|
def testTopoShapeMakeThickness(self):
|
|
# Act
|
|
thickness = self.doc.Box1.Shape.makeThickness(self.doc.Box1.Shape.Faces[0:2], 0.1, 0.0001)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if thickness.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(thickness.ElementMapSize, 74)
|
|
|
|
def testTopoShapeMakeOffsetShape(self):
|
|
# Act
|
|
offset = self.doc.Box1.Shape.Faces[0].makeOffset(1)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if offset.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(offset.ElementMapSize, 17)
|
|
|
|
def testTopoShapeOffset2D(self):
|
|
# Act
|
|
offset = self.doc.Box1.Shape.Faces[0].makeOffset2D(1)
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if offset.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(offset.ElementMapSize, 17)
|
|
|
|
def testTopoShapeRemoveSplitter(self):
|
|
# Act
|
|
fused = self.doc.Box1.Shape.fuse(self.doc.Box2.Shape)
|
|
removed = fused.removeSplitter()
|
|
self.doc.recompute()
|
|
# Assert elementMap
|
|
if removed.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(removed.ElementMapSize, 38)
|
|
|
|
def testTopoShapeCompSolid(self):
|
|
# Act
|
|
compSolid = Part.CompSolid([self.doc.Box1.Shape, self.doc.Box2.Shape]) # list of subobjects
|
|
box1ts = self.doc.Box1.Shape
|
|
compSolid.add(box1ts.Solids[0])
|
|
# Assert elementMap
|
|
if compSolid.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(compSolid.ElementMapSize, 78)
|
|
|
|
def testTopoShapeFaceOffset(self):
|
|
# Arrange
|
|
box_toposhape = self.doc.Box1.Shape
|
|
# Act
|
|
offset = box_toposhape.Faces[0].makeOffset(2.0)
|
|
# Assert elementMap
|
|
if box_toposhape.Faces[0].ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(
|
|
box_toposhape.Faces[0].ElementMapSize, 9
|
|
) # 1 Face, 4 Edges, 4 Vertexes
|
|
self.assertEqual(offset.ElementMapSize, 17) # 1 Face, 8 Edges, 8 Vertexes
|
|
|
|
# Todo: makeEvolved doesn't work right, probably due to missing c++ code.
|
|
# def testTopoShapeFaceEvolve(self):
|
|
# # Arrange
|
|
# box_toposhape = self.doc.Box1.Shape
|
|
# # Act
|
|
# evolved = box_toposhape.Faces[0].makeEvolved(self.doc.Box1.Shape.Wires[1]) # 2,3,4,5 bad
|
|
# # Assert elementMap
|
|
# if box_toposhape.Faces[0].ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
# self.assertEqual(box_toposhape.Faces[0].ElementMapSize, 9) # 1 Face, 4 Edges, 4 Vertexes
|
|
# self.assertEqual(evolved.ElementMapSize, 0) # Todo: This can't be correct.
|
|
|
|
def testTopoShapePart(self):
|
|
# Arrange
|
|
box1ts = self.doc.Box1.Shape
|
|
face1 = box1ts.Faces[0]
|
|
box1ts2 = box1ts.copy()
|
|
# Act
|
|
face2 = box1ts.getElement("Face2")
|
|
indexed_name = box1ts.findSubShape(face1)
|
|
faces1 = box1ts.findSubShapesWithSharedVertex(face2)
|
|
subshapes1 = box1ts.getChildShapes("Solid1")
|
|
# box1ts.clearCache() # Todo: no apparent way to see a result at this level
|
|
# Assert
|
|
self.assertTrue(face2.isSame(box1ts.Faces[1]))
|
|
self.assertEqual(indexed_name[0], "Face")
|
|
self.assertEqual(indexed_name[1], 1)
|
|
self.assertEqual(len(faces1), 1)
|
|
self.assertTrue(faces1[0].isSame(box1ts.Faces[1]))
|
|
self.assertEqual(len(subshapes1), 1)
|
|
self.assertTrue(subshapes1[0].isSame(box1ts.Solids[0]))
|
|
|
|
def testTopoShapeMapSubElement(self):
|
|
# Arrange
|
|
box = Part.makeBox(1, 2, 3)
|
|
# face = box.Faces[0] # Do not do this. Need the subelement call each usage.
|
|
# Assert everything empty
|
|
self.assertEqual(box.ElementMapSize, 0)
|
|
self.assertEqual(box.Faces[0].ElementMapSize, 0)
|
|
# Act
|
|
box.mapSubElement(box.Faces[0])
|
|
# Assert elementMaps created
|
|
if box.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertEqual(box.ElementMapSize, 9) # 1 Face, 4 Edges, 4 Vertexes
|
|
self.assertEqual(box.Faces[0].ElementMapSize, 9)
|
|
|
|
def testTopoShapeGetElementHistory(self):
|
|
self.doc.addObject("Part::Fuse", "Fuse")
|
|
self.doc.Fuse.Base = self.doc.Box1
|
|
self.doc.Fuse.Tool = self.doc.Box2
|
|
# Act
|
|
self.doc.recompute()
|
|
fuse1 = self.doc.Fuse.Shape
|
|
if fuse1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
history1 = fuse1.getElementHistory(fuse1.ElementReverseMap["Vertex1"])
|
|
# Assert
|
|
self.assertEqual(len(history1), 3) # Just the Fuse operation
|
|
|
|
# Todo: Still broken, still can't find parms that consistently work to test this.
|
|
# However, the results with an empty elementMap are consistent with making the
|
|
# same calls on LS3. So what this method is supposed to do remains a mystery;
|
|
# So far, it just wipes out the elementMap and returns the Toposhape.
|
|
# def testTopoShapeMapShapes(self):
|
|
# self.doc.addObject("Part::Fuse", "Fuse")
|
|
# self.doc.Fuse.Base = self.doc.Box1
|
|
# self.doc.Fuse.Tool = self.doc.Box2
|
|
# # Act
|
|
# self.doc.recompute()
|
|
# fuse1 = self.doc.Fuse.Shape
|
|
# res = fuse1.copy() # Make it mutable
|
|
# self.assertEqual(res.ElementMapSize,58)
|
|
# result = res.mapShapes([(fuse1, fuse1.Faces[0])], []) #[(res, res.Vertexes[0])])
|
|
# self.assertEqual(res.ElementMapSize,9)
|
|
# # result2 = fuse1.copy().mapShapes([],[(fuse1, fuse1.Edges[0]),(fuse1, fuse1.Edges[1])])
|
|
# self.assertEqual(fuse1.ElementMapSize,58) #
|
|
# self.assertEqual(fuse1.Faces[0].ElementMapSize,9)
|
|
# self.assertEqual(result.ElementMapSize,9)
|
|
# self.assertEqual(result.Faces[0].ElementMapSize,0)
|
|
# self.assertEqual(result2.ElementMapSize,9)
|
|
# self.assertEqual(result2.Faces[0].ElementMapSize,0)
|
|
|
|
def testPartCompoundCut1(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Cut", "Cut")
|
|
self.doc.Cut.Base = self.doc.Cylinder1
|
|
self.doc.Cut.Tool = self.doc.Compound1
|
|
# Act
|
|
self.doc.recompute()
|
|
cut1 = self.doc.Cut.Shape
|
|
# Assert elementMap
|
|
refkeys = [
|
|
"Vertex6",
|
|
"Vertex5",
|
|
"Edge7",
|
|
"Edge8",
|
|
"Edge9",
|
|
"Edge5",
|
|
"Edge6",
|
|
"Face4",
|
|
"Face2",
|
|
"Edge1",
|
|
"Vertex4",
|
|
"Edge4",
|
|
"Vertex3",
|
|
"Edge2",
|
|
"Edge3",
|
|
"Face1",
|
|
"Face5",
|
|
"Face3",
|
|
"Vertex1",
|
|
"Vertex2",
|
|
]
|
|
if cut1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertKeysInMap(cut1.ElementReverseMap, refkeys)
|
|
self.assertEqual(len(cut1.ElementReverseMap.keys()), len(refkeys))
|
|
# Assert Volume
|
|
self.assertAlmostEqual(cut1.Volume, self.doc.Cylinder1.Shape.Volume * (3 / 4))
|
|
|
|
def testPartCompoundCut2(self):
|
|
# Arrange
|
|
self.doc.addObject("Part::Cut", "Cut")
|
|
self.doc.Cut.Base = self.doc.Compound1
|
|
self.doc.Cut.Tool = self.doc.Cylinder1
|
|
self.doc.Cut.Refine = False
|
|
# Act
|
|
self.doc.recompute()
|
|
cut1 = self.doc.Cut.Shape
|
|
# Assert elementMap
|
|
refkeys = [
|
|
"Vertex3",
|
|
"Vertex4",
|
|
"Vertex8",
|
|
"Vertex10",
|
|
"Vertex7",
|
|
"Vertex9",
|
|
"Vertex13",
|
|
"Vertex14",
|
|
"Vertex18",
|
|
"Vertex20",
|
|
"Vertex17",
|
|
"Vertex19",
|
|
"Edge3",
|
|
"Edge15",
|
|
"Edge9",
|
|
"Edge12",
|
|
"Edge13",
|
|
"Edge11",
|
|
"Edge8",
|
|
"Edge17",
|
|
"Edge18",
|
|
"Edge19",
|
|
"Edge30",
|
|
"Edge24",
|
|
"Edge27",
|
|
"Edge28",
|
|
"Edge29",
|
|
"Edge25",
|
|
"Edge26",
|
|
"Edge23",
|
|
"Face7",
|
|
"Face4",
|
|
"Face8",
|
|
"Face14",
|
|
"Face13",
|
|
"Face11",
|
|
"Face12",
|
|
"Face10",
|
|
"Edge22",
|
|
"Vertex12",
|
|
"Edge20",
|
|
"Vertex11",
|
|
"Edge21",
|
|
"Edge16",
|
|
"Face9",
|
|
"Vertex15",
|
|
"Vertex16",
|
|
]
|
|
if cut1.ElementMapVersion != "": # Should be '4' as of Mar 2023.
|
|
self.assertKeysInMap(cut1.ElementReverseMap, refkeys)
|
|
self.assertEqual(len(cut1.ElementReverseMap.keys()), len(refkeys))
|