"""Shape hashing and equality — the contract cad/topology/geom_memo.py rests on. Verified against the stock wheel before being replicated here: upstream binds __hash__ (TShape + Location) and leaves __eq__ at Python's default identity comparison. That pairing is deliberate on our side too. geom_memo buckets on hash(face) and disambiguates with IsSame precisely because == cannot be trusted; binding __eq__ to IsEqual would collapse entries that memo expects to keep apart, changing behaviour while looking like an improvement. """ from __future__ import annotations from OCP.gp import gp_Trsf, gp_Vec from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE from OCP.TopExp import TopExp from OCP.TopLoc import TopLoc_Location from OCP.TopTools import TopTools_IndexedMapOfShape def _faces(shape): faces = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(shape, TopAbs_FACE, faces) return faces def test_re_extracted_face_hashes_equal_and_is_same(fixture_shapes): shape = fixture_shapes["box"] f1 = _faces(shape).FindKey(1) f2 = _faces(shape).FindKey(1) assert f1 is not f2 # separate extractions, separate wrappers assert hash(f1) == hash(f2) assert f1.IsSame(f2) assert f1.IsEqual(f2) def test_eq_is_identity_not_isequal(fixture_shapes): shape = fixture_shapes["box"] f1 = _faces(shape).FindKey(1) f2 = _faces(shape).FindKey(1) # Matches the stock wheel: equal hashes, unequal objects. assert f1 != f2 assert f1 == f1 def test_moved_copy_hashes_differently(fixture_shapes): trsf = gp_Trsf() trsf.SetTranslation(gp_Vec(1.0, 0.0, 0.0)) face = _faces(fixture_shapes["box"]).FindKey(1) moved = face.Moved(TopLoc_Location(trsf)) assert hash(moved) != hash(face) assert not face.IsSame(moved) def test_orientation_is_not_part_of_the_hash(fixture_shapes): # Area and centre of mass are orientation-independent, which is why the # memo's key needs no orientation component. face = _faces(fixture_shapes["box"]).FindKey(1) reversed_face = face.Reversed() assert hash(reversed_face) == hash(face) assert face.IsSame(reversed_face) assert not face.IsEqual(reversed_face) def test_subshapes_outlive_their_container(fixture_shapes): """The lifetime class that segfaulted a process-global face memo.""" faces = _faces(fixture_shapes["fused"]) picked = [faces.FindKey(i) for i in range(1, faces.Extent() + 1)] del faces # Every wrapper owns its own copy, so the map's death is irrelevant. assert all(not f.IsNull() for f in picked) assert len({hash(f) for f in picked}) == len(picked) def test_explorer_results_outlive_the_explorer(fixture_shapes): from OCP.TopExp import TopExp_Explorer exp = TopExp_Explorer(fixture_shapes["box"], TopAbs_EDGE) edges = [] while exp.More(): edges.append(exp.Current()) exp.Next() del exp assert len(edges) == 24 # a box: 12 edges, each shared by two faces assert all(not e.IsNull() for e in edges) def test_indexed_map_contains_uses_is_same(fixture_shapes): shape = fixture_shapes["box"] faces = _faces(shape) other = _faces(shape).FindKey(3) assert faces.Contains(other) assert faces.FindIndex(other) == 3