Covering Face.axes_of_symmetry corner cases

This commit is contained in:
gumyr 2025-02-08 14:46:13 -05:00
parent b64ab34407
commit f22f54af5f
2 changed files with 245 additions and 6 deletions

View file

@ -57,7 +57,7 @@ from __future__ import annotations
import copy
import warnings
from typing import Any, Tuple, Union, overload, TYPE_CHECKING
from typing import Any, overload, TYPE_CHECKING
from collections.abc import Iterable, Sequence
@ -93,6 +93,7 @@ from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Shell, TopoDS_Solid
from OCP.gce import gce_MakeLin
from OCP.gp import gp_Pnt, gp_Vec
from build123d.build_enums import CenterOf, GeomType, Keep, SortBy, Transition
from build123d.geometry import (
TOLERANCE,
@ -406,7 +407,6 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
ValueError: If the face or its underlying representation is empty.
ValueError: If the face is not planar.
"""
if self.wrapped is None:
raise ValueError("Can't determine axes_of_symmetry of empty face")
@ -452,12 +452,42 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
x_dir=cross_dir,
z_dir=cross_dir.cross(normal),
)
# Split by plane
top, bottom = self.split(split_plane, keep=Keep.BOTH)
top_flipped = top.mirror(split_plane)
if type(top) != type(bottom): # exit early if not same
continue
if top is None or bottom is None: # Impossible to actually happen?
continue
top_list = ShapeList(top if isinstance(top, list) else [top])
bottom_list = ShapeList(bottom if isinstance(top, list) else [bottom])
if len(top_list) != len(bottom_list): # exit early unequal length
continue
bottom_list = bottom_list.sort_by(Axis(cog, cross_dir))
top_flipped_list = ShapeList(
f.mirror(split_plane) for f in top_list
).sort_by(Axis(cog, cross_dir))
bottom_area = sum(f.area for f in bottom_list)
intersect_area = 0.0
for flipped_face, bottom_face in zip(top_flipped_list, bottom_list):
intersection = flipped_face.intersect(bottom_face)
if intersection is None or isinstance(intersection, list):
intersect_area = -1.0
break
else:
assert isinstance(intersection, Face)
intersect_area += intersection.area
if intersect_area == -1.0:
continue
# Are the top/bottom the same?
if abs(bottom.intersect(top_flipped).area - bottom.area) < TOLERANCE:
# If this axis isn't in the set already add it
if abs(intersect_area - bottom_area) < TOLERANCE:
if not symmetry_dirs:
symmetry_dirs.add(cross_dir)
else:

View file

@ -40,7 +40,7 @@ from build123d.build_sketch import BuildSketch
from build123d.exporters3d import export_stl
from build123d.geometry import Axis, Location, Plane, Pos, Vector
from build123d.importers import import_stl
from build123d.objects_curve import Polyline
from build123d.objects_curve import Line, Polyline, Spline, ThreePointArc
from build123d.objects_part import Box, Cylinder
from build123d.objects_sketch import (
Circle,
@ -547,6 +547,215 @@ class TestFace(unittest.TestCase):
self.assertTrue(all(a == t) for a, t in zip(axes_dirs, target_dirs))
self.assertTrue(all(a.position == cog) for a in axes)
# Fast abort code paths
s1 = Spline(
(0.0293923441471, 1.9478225275438),
(0.0293923441471, 1.2810839877038),
(0, -0.0521774724562),
(0.0293923441471, -1.3158620329962),
(0.0293923441471, -1.9478180575162),
)
l1 = Line(s1 @ 1, s1 @ 0)
self.assertEqual(len(Face(Wire([s1, l1])).axes_of_symmetry), 0)
with BuildSketch() as skt:
with BuildLine():
Line(
(-13.186467340991, 2.3737403364651),
(-5.1864673409911, 2.3737403364651),
)
Line(
(-13.186467340991, 2.3737403364651),
(-13.186467340991, -2.4506956262169),
)
ThreePointArc(
(-13.186467340991, -2.4506956262169),
(-13.479360559805, -3.1578024074034),
(-14.186467340991, -3.4506956262169),
)
Line(
(-17.186467340991, -3.4506956262169),
(-14.186467340991, -3.4506956262169),
)
ThreePointArc(
(-17.186467340991, -3.4506956262169),
(-17.893574122178, -3.1578024074034),
(-18.186467340991, -2.4506956262169),
)
Line(
(-18.186467340991, 7.6644400497781),
(-18.186467340991, -2.4506956262169),
)
Line(
(-51.186467340991, 7.6644400497781),
(-18.186467340991, 7.6644400497781),
)
Line(
(-51.186467340991, 7.6644400497781),
(-51.186467340991, -5.5182296356389),
)
Line(
(-51.186467340991, -5.5182296356389),
(-33.186467340991, -5.5182296356389),
)
Line(
(-33.186467340991, -5.5182296356389),
(-33.186467340991, -5.3055423052429),
)
Line(
(-33.186467340991, -5.3055423052429),
(53.813532659009, -5.3055423052429),
)
Line(
(53.813532659009, -5.3055423052429),
(53.813532659009, -5.7806956262169),
)
Line(
(66.813532659009, -5.7806956262169),
(53.813532659009, -5.7806956262169),
)
Line(
(66.813532659009, -2.7217530775369),
(66.813532659009, -5.7806956262169),
)
Line(
(54.813532659009, -2.7217530775369),
(66.813532659009, -2.7217530775369),
)
Line(
(54.813532659009, 7.6644400497781),
(54.813532659009, -2.7217530775369),
)
Line(
(38.813532659009, 7.6644400497781),
(54.813532659009, 7.6644400497781),
)
Line(
(38.813532659009, 7.6644400497781),
(38.813532659009, -2.4506956262169),
)
ThreePointArc(
(38.813532659009, -2.4506956262169),
(38.520639440195, -3.1578024074034),
(37.813532659009, -3.4506956262169),
)
Line(
(37.813532659009, -3.4506956262169),
(34.813532659009, -3.4506956262169),
)
ThreePointArc(
(34.813532659009, -3.4506956262169),
(34.106425877822, -3.1578024074034),
(33.813532659009, -2.4506956262169),
)
Line(
(33.813532659009, 2.3737403364651),
(33.813532659009, -2.4506956262169),
)
Line(
(25.813532659009, 2.3737403364651),
(33.813532659009, 2.3737403364651),
)
Line(
(25.813532659009, 2.3737403364651),
(25.813532659009, -2.4506956262169),
)
ThreePointArc(
(25.813532659009, -2.4506956262169),
(25.520639440195, -3.1578024074034),
(24.813532659009, -3.4506956262169),
)
Line(
(24.813532659009, -3.4506956262169),
(21.813532659009, -3.4506956262169),
)
ThreePointArc(
(21.813532659009, -3.4506956262169),
(21.106425877822, -3.1578024074034),
(20.813532659009, -2.4506956262169),
)
Line(
(20.813532659009, 2.3737403364651),
(20.813532659009, -2.4506956262169),
)
Line(
(12.813532659009, 2.3737403364651),
(20.813532659009, 2.3737403364651),
)
Line(
(12.813532659009, 2.3737403364651),
(12.813532659009, -2.4506956262169),
)
ThreePointArc(
(12.813532659009, -2.4506956262169),
(12.520639440195, -3.1578024074034),
(11.813532659009, -3.4506956262169),
)
Line(
(8.8135326590089, -3.4506956262169),
(11.813532659009, -3.4506956262169),
)
ThreePointArc(
(8.8135326590089, -3.4506956262169),
(8.1064258778223, -3.1578024074034),
(7.8135326590089, -2.4506956262169),
)
Line(
(7.8135326590089, 2.3737403364651),
(7.8135326590089, -2.4506956262169),
)
Line(
(-0.1864673409911, 2.3737403364651),
(7.8135326590089, 2.3737403364651),
)
Line(
(-0.1864673409911, 2.3737403364651),
(-0.1864673409911, -2.4506956262169),
)
ThreePointArc(
(-0.1864673409911, -2.4506956262169),
(-0.4793605598046, -3.1578024074034),
(-1.1864673409911, -3.4506956262169),
)
Line(
(-4.1864673409911, -3.4506956262169),
(-1.1864673409911, -3.4506956262169),
)
ThreePointArc(
(-4.1864673409911, -3.4506956262169),
(-4.8935741221777, -3.1578024074034),
(-5.1864673409911, -2.4506956262169),
)
Line(
(-5.1864673409911, 2.3737403364651),
(-5.1864673409911, -2.4506956262169),
)
make_face()
self.assertEqual(len(skt.face().axes_of_symmetry), 0)
class TestAxesOfSymmetrySplitNone(unittest.TestCase):
def test_split_returns_none(self):
# Create a rectangle face for testing.
rect = Rectangle(10, 5).face()
# Monkey-patch the split method to simulate the degenerate case:
# Force split to return (None, rect) for any splitting plane.
original_split = Face.split # Save the original split method.
Face.split = lambda self, plane, keep: (None, None)
# Call axes_of_symmetry. With our patch, every candidate axis is skipped,
# so we expect no symmetry axes to be found.
axes = rect.axes_of_symmetry
# Verify that the result is an empty list.
self.assertEqual(
axes, [], "Expected no symmetry axes when split returns None for one half."
)
# Restore the original split method (cleanup).
Face.split = original_split
if __name__ == "__main__":
unittest.main()