topologicpy 0.8.10__py3-none-any.whl → 0.8.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
topologicpy/Topology.py CHANGED
@@ -250,77 +250,6 @@ class Topology():
250
250
 
251
251
  topology = Topology.AddContent(topology, apertures, subTopologyType=subTopologyType, tolerance=tolerance)
252
252
  return topology
253
-
254
-
255
-
256
- @staticmethod
257
- def AddApertures_old(topology, apertures, exclusive=False, subTopologyType=None, tolerance=0.001):
258
- """
259
- Adds the input list of apertures to the input topology or to its subtopologies based on the input subTopologyType.
260
-
261
- Parameters
262
- ----------
263
- topology : topologic_core.Topology
264
- The input topology.
265
- apertures : list
266
- The input list of apertures.
267
- exclusive : bool , optional
268
- If set to True, one (sub)topology will accept only one aperture. Otherwise, one (sub)topology can accept multiple apertures. The default is False.
269
- subTopologyType : string , optional
270
- The subtopology type to which to add the apertures. This can be "cell", "face", "edge", or "vertex". It is case insensitive. If set to None, the apertures will be added to the input topology. The default is None.
271
- tolerance : float , optional
272
- The desired tolerance. The default is 0.001. This is larger than the usual 0.0001 as it seems to work better.
273
-
274
- Returns
275
- -------
276
- topologic_core.Topology
277
- The input topology with the apertures added to it.
278
-
279
- """
280
- from topologicpy.Vertex import Vertex
281
- from topologicpy.Cluster import Cluster
282
- from topologicpy.Aperture import Aperture
283
- def processApertures(subTopologies, apertures, exclusive=False, tolerance=0.001):
284
- usedTopologies = []
285
- for subTopology in subTopologies:
286
- usedTopologies.append(0)
287
- ap = 1
288
- for aperture in apertures:
289
- apCenter = Topology.InternalVertex(aperture, tolerance=tolerance)
290
- for i in range(len(subTopologies)):
291
- subTopology = subTopologies[i]
292
- if exclusive == True and usedTopologies[i] == 1:
293
- continue
294
- if Vertex.Distance(apCenter, subTopology) <= tolerance:
295
- context = topologic.Context.ByTopologyParameters(subTopology, 0.5, 0.5, 0.5)
296
- _ = Aperture.ByTopologyContext(aperture, context)
297
- if exclusive == True:
298
- usedTopologies[i] = 1
299
- ap = ap + 1
300
- return None
301
-
302
- if not Topology.IsInstance(topology, "Topology"):
303
- print("Topology.AddApertures - Error: The input topology parameter is not a valid topology. Returning None.")
304
- return None
305
- if not apertures:
306
- return topology
307
- if not isinstance(apertures, list):
308
- print("Topology.AddApertures - Error: the input apertures parameter is not a list. Returning None.")
309
- return None
310
- apertures = [x for x in apertures if Topology.IsInstance(x , "Topology")]
311
- if len(apertures) < 1:
312
- return topology
313
- if not subTopologyType:
314
- subTopologyType = "self"
315
- if not subTopologyType.lower() in ["self", "cell", "face", "edge", "vertex"]:
316
- print("Topology.AddApertures - Error: the input subtopology type parameter is not a recognized type. Returning None.")
317
- return None
318
- if subTopologyType.lower() == "self":
319
- subTopologies = [topology]
320
- else:
321
- subTopologies = Topology.SubTopologies(topology, subTopologyType)
322
- processApertures(subTopologies, apertures, exclusive, tolerance=tolerance)
323
- return topology
324
253
 
325
254
  @staticmethod
326
255
  def AddContent(topology, contents, subTopologyType=None, tolerance=0.0001):
@@ -1486,7 +1415,6 @@ class Topology():
1486
1415
  st = None
1487
1416
  return st
1488
1417
 
1489
-
1490
1418
  @staticmethod
1491
1419
  def ByGeometry(vertices=[], edges=[], faces=[], topologyType: str = None, tolerance: float = 0.0001, silent: bool = False):
1492
1420
  """
@@ -1625,180 +1553,6 @@ class Topology():
1625
1553
  else:
1626
1554
  returnTopology = Cluster.ByTopologies(topVerts)
1627
1555
  return returnTopology
1628
-
1629
-
1630
-
1631
-
1632
-
1633
-
1634
- @staticmethod
1635
- def ByGeometry_old(vertices=[], edges=[], faces=[], color=[1.0, 1.0, 1.0, 1.0], id=None, name=None, lengthUnit="METERS", outputMode="default", tolerance=0.0001):
1636
- """
1637
- Create a topology by the input lists of vertices, edges, and faces.
1638
-
1639
- Parameters
1640
- ----------
1641
- vertices : list
1642
- The input list of vertices in the form of [x, y, z]
1643
- edges : list , optional
1644
- The input list of edges in the form of [i, j] where i and j are vertex indices.
1645
- faces : list , optional
1646
- The input list of faces in the form of [i, j, k, l, ...] where the items in the list are vertex indices. The face is assumed to be closed to the last vertex is connected to the first vertex automatically.
1647
- color : list , optional
1648
- The desired color of the object in the form of [r, g, b, a] where the components are between 0 and 1 and represent red, blue, green, and alpha (transparency) respectively. The default is [1.0, 1.0, 1.0, 1.0].
1649
- id : str , optional
1650
- The desired ID of the object. If set to None, an automatic uuid4 will be assigned to the object. The default is None.
1651
- name : str , optional
1652
- The desired name of the object. If set to None, a default name "Topologic_[topology_type]" will be assigned to the object. The default is None.
1653
- lengthUnit : str , optional
1654
- The length unit used for the object. The default is "METERS"
1655
- outputMode : str , optional
1656
- The desired output mode of the object. This can be "wire", "shell", "cell", "cellcomplex", or "default". It is case insensitive. The default is "default".
1657
- tolerance : float , optional
1658
- The desired tolerance. The default is 0.0001.
1659
-
1660
- Returns
1661
- -------
1662
- topology : topologic_core.Topology
1663
- The created topology. The topology will have a dictionary embedded in it that records the input attributes (color, id, lengthUnit, name, type)
1664
-
1665
- """
1666
- def topologyByFaces(faces, outputMode, tolerance=0.0001):
1667
- output = None
1668
- if len(faces) == 1:
1669
- return faces[0]
1670
- if outputMode.lower() == "cell":
1671
- output = Cell.ByFaces(faces, tolerance=tolerance)
1672
- if output:
1673
- return output
1674
- else:
1675
- return None
1676
- if outputMode.lower() == "cellcomplex":
1677
- output = CellComplex.ByFaces(faces, tolerance=tolerance)
1678
- if output:
1679
- return output
1680
- else:
1681
- return None
1682
- if outputMode.lower() == "shell":
1683
- output = Shell.ByFaces(faces, tolerance=tolerance) # This can return a list
1684
- if Topology.IsInstance(output, "Shell"):
1685
- return output
1686
- else:
1687
- return None
1688
- if outputMode.lower() == "default":
1689
- output = Cluster.ByTopologies(faces)
1690
- if output:
1691
- return output
1692
- return output
1693
- def topologyByEdges(edges, outputMode):
1694
- output = None
1695
- if len(edges) == 1:
1696
- return edges[0]
1697
- output = Cluster.ByTopologies(edges)
1698
- if outputMode.lower() == "wire":
1699
- output = Topology.SelfMerge(output, tolerance=tolerance)
1700
- if Topology.IsInstance(output, "Wire"):
1701
- return output
1702
- else:
1703
- return None
1704
- return output
1705
- def edgesByVertices(vertices, topVerts):
1706
- if len(vertices) < 2:
1707
- return []
1708
- edges = []
1709
- for i in range(len(vertices)-1):
1710
- v1 = vertices[i]
1711
- v2 = vertices[i+1]
1712
- e1 = Edge.ByVertices([topVerts[v1], topVerts[v2]], tolerance=tolerance)
1713
- edges.append(e1)
1714
- # connect the last vertex to the first one
1715
- v1 = vertices[-1]
1716
- v2 = vertices[0]
1717
- e1 = Edge.ByVertices([topVerts[v1], topVerts[v2]], tolerance=tolerance)
1718
- edges.append(e1)
1719
- return edges
1720
- from topologicpy.Vertex import Vertex
1721
- from topologicpy.Edge import Edge
1722
- from topologicpy.Wire import Wire
1723
- from topologicpy.Face import Face
1724
- from topologicpy.Shell import Shell
1725
- from topologicpy.Cell import Cell
1726
- from topologicpy.CellComplex import CellComplex
1727
- from topologicpy.Cluster import Cluster
1728
- from topologicpy.Dictionary import Dictionary
1729
- import uuid
1730
- returnTopology = None
1731
- topVerts = []
1732
- topEdges = []
1733
- topFaces = []
1734
- vertices = [v for v in vertices if not len(v) == 0]
1735
- edges = [e for e in edges if not len(e) == 0]
1736
- faces = [f for f in faces if not len(f) == 0]
1737
- if len(vertices) > 0:
1738
- for aVertex in vertices:
1739
- v = Vertex.ByCoordinates(aVertex[0], aVertex[1], aVertex[2])
1740
- topVerts.append(v)
1741
- else:
1742
- return None
1743
- if (outputMode.lower == "wire") and (len(edges) > 0):
1744
- for anEdge in edges:
1745
- topEdge = Edge.ByVertices([topVerts[anEdge[0]], topVerts[anEdge[1]]], tolerance=tolerance)
1746
- topEdges.append(topEdge)
1747
- if len(topEdges) > 0:
1748
- returnTopology = topologyByEdges(topEdges)
1749
- elif len(faces) > 0:
1750
- for aFace in faces:
1751
- faceEdges = edgesByVertices(aFace, topVerts)
1752
- if len(faceEdges) > 2:
1753
- faceWire = Wire.ByEdges(faceEdges, tolerance=tolerance)
1754
- try:
1755
- topFace = Face.ByWire(faceWire, tolerance=tolerance, silent=True)
1756
- if Topology.IsInstance(topFace, "Face"):
1757
- topFaces.append(topFace)
1758
- elif isinstance(topFace, list):
1759
- topFaces += topFace
1760
- except:
1761
- pass
1762
- if len(topFaces) > 0:
1763
- returnTopology = topologyByFaces(topFaces, outputMode=outputMode, tolerance=tolerance)
1764
- elif len(edges) > 0:
1765
- for anEdge in edges:
1766
- topEdge = Edge.ByVertices([topVerts[anEdge[0]], topVerts[anEdge[1]]], tolerance=tolerance)
1767
- topEdges.append(topEdge)
1768
- if len(topEdges) > 0:
1769
- returnTopology = topologyByEdges(topEdges, outputMode=outputMode)
1770
- else:
1771
- returnTopology = Cluster.ByTopologies(topVerts)
1772
- if returnTopology:
1773
- keys = []
1774
- values = []
1775
- keys.append("TOPOLOGIC_color")
1776
- keys.append("TOPOLOGIC_id")
1777
- keys.append("TOPOLOGIC_name")
1778
- keys.append("TOPOLOGIC_type")
1779
- keys.append("TOPOLOGIC_length_unit")
1780
- if color:
1781
- if isinstance(color, tuple):
1782
- color = list(color)
1783
- elif isinstance(color, list):
1784
- if isinstance(color[0], tuple):
1785
- color = list(color[0])
1786
- values.append(color)
1787
- else:
1788
- values.append([1.0, 1.0, 1.0, 1.0])
1789
- if id:
1790
- values.append(id)
1791
- else:
1792
- values.append(str(uuid.uuid4()))
1793
- if name:
1794
- values.append(name)
1795
- else:
1796
- values.append("Topologic_"+Topology.TypeAsString(returnTopology))
1797
- values.append(Topology.TypeAsString(returnTopology))
1798
- values.append(lengthUnit)
1799
- topDict = Dictionary.ByKeysValues(keys, values)
1800
- Topology.SetDictionary(returnTopology, topDict)
1801
- return returnTopology
1802
1556
 
1803
1557
  @staticmethod
1804
1558
  def ByBIMPath(path, guidKey: str = "guid", colorKey: str = "color", typeKey: str = "type",
@@ -4168,6 +3922,7 @@ class Topology():
4168
3922
  from topologicpy.Grid import Grid
4169
3923
  from topologicpy.Matrix import Matrix
4170
3924
  import numpy as np
3925
+ from itertools import permutations
4171
3926
 
4172
3927
  def generate_floats(n):
4173
3928
  if n < 2:
@@ -4232,16 +3987,34 @@ class Topology():
4232
3987
  if np.dot(eigenvectors[:, i], [1, 0, 0]) < 0:
4233
3988
  eigenvectors[:, i] *= -1
4234
3989
 
4235
- # Step 8: Create the rotation matrix
4236
- rotation_matrix = np.eye(4)
4237
- rotation_matrix[:3, :3] = eigenvectors.T # Use transpose to align points to axes
3990
+ # Step 8: Generate all permutations of aligning principal axes to world axes
3991
+ world_axes = np.eye(3) # X, Y, Z unit vectors
3992
+ permutations_axes = list(permutations(world_axes))
3993
+
3994
+ # Create a list to hold all canonical matrices
3995
+ canonical_matrices = []
3996
+
3997
+ for perm in permutations_axes:
3998
+ # Construct the rotation matrix for this permutation
3999
+ rotation_matrix = np.eye(4)
4000
+ rotation_matrix[:3, :3] = np.array(perm).T @ eigenvectors.T # Align points to axes for this permutation
4001
+
4002
+ # Combine transformations: scale -> translate -> rotate
4003
+ combined_matrix = Matrix.Multiply(rotation_matrix.tolist(), scaling_matrix)
4004
+ combined_matrix = Matrix.Multiply(combined_matrix, translation_matrix)
4005
+
4006
+ # Add the combined matrix to the list
4007
+ canonical_matrices.append(combined_matrix)
4008
+ # # Step 8: Create the rotation matrix
4009
+ # rotation_matrix = np.eye(4)
4010
+ # rotation_matrix[:3, :3] = eigenvectors.T # Use transpose to align points to axes
4238
4011
 
4239
- # Step 9: Rotate the object to align it
4240
- transformation_matrix = Matrix.Multiply(scaling_matrix, translation_matrix)
4241
- transformation_matrix = Matrix.Multiply(rotation_matrix.tolist(), transformation_matrix)
4012
+ # # Step 9: Rotate the object to align it
4013
+ # transformation_matrix = Matrix.Multiply(scaling_matrix, translation_matrix)
4014
+ # transformation_matrix = Matrix.Multiply(rotation_matrix.tolist(), transformation_matrix)
4242
4015
 
4243
4016
  # Step 10: Return the resulting matrix
4244
- return transformation_matrix
4017
+ return canonical_matrices
4245
4018
 
4246
4019
  @staticmethod
4247
4020
  def CenterOfMass(topology):
@@ -4459,127 +4232,6 @@ class Topology():
4459
4232
  final_clusters.append(Topology.SelfMerge(Cluster.ByTopologies(cluster), tolerance=tolerance))
4460
4233
  return final_clusters
4461
4234
 
4462
- @staticmethod
4463
- def ClusterFaces_orig(topology, angTolerance=0.1, tolerance=0.0001):
4464
- """
4465
- Clusters the faces of the input topology by their direction.
4466
-
4467
- Parameters
4468
- ----------
4469
- topology : topologic_core.Topology
4470
- The input topology.
4471
- angTolerance : float , optional
4472
- The desired angular tolerance. The default is 0.1.
4473
- tolerance : float, optional
4474
- The desired tolerance. The default is 0.0001.
4475
-
4476
- Returns
4477
- -------
4478
- list
4479
- The list of clusters of faces where faces in the same cluster have the same direction.
4480
-
4481
- """
4482
- from topologicpy.Face import Face
4483
- from topologicpy.Cluster import Cluster
4484
-
4485
- def angle_between(v1, v2):
4486
- u1 = v1 / norm(v1)
4487
- u2 = v2 / norm(v2)
4488
- y = u1 - u2
4489
- x = u1 + u2
4490
- if norm(x) == 0:
4491
- return 0
4492
- a0 = 2 * arctan(norm(y) / norm(x))
4493
- if (not signbit(a0)) or signbit(pi - a0):
4494
- return a0
4495
- elif signbit(a0):
4496
- return 0
4497
- else:
4498
- return pi
4499
-
4500
- def collinear(v1, v2, tol):
4501
- ang = angle_between(v1, v2)
4502
- if math.isnan(ang) or math.isinf(ang):
4503
- raise Exception("Face.IsCollinear - Error: Could not determine the angle between the input faces")
4504
- elif abs(ang) < tol or abs(pi - ang) < tol:
4505
- return True
4506
- return False
4507
-
4508
- def sumRow(matrix, i):
4509
- return np.sum(matrix[i,:])
4510
-
4511
- def buildSimilarityMatrix(samples, tol):
4512
- numOfSamples = len(samples)
4513
- matrix = np.zeros(shape=(numOfSamples, numOfSamples))
4514
- for i in range(len(matrix)):
4515
- for j in range(len(matrix)):
4516
- if collinear(samples[i], samples[j], tol):
4517
- matrix[i, j] = 1
4518
- return matrix
4519
-
4520
- def determineRow(matrix):
4521
- maxNumOfOnes = -1
4522
- row = -1
4523
- for i in range(len(matrix)):
4524
- if maxNumOfOnes < sumRow(matrix, i):
4525
- maxNumOfOnes = sumRow(matrix, i)
4526
- row = i
4527
- return row
4528
-
4529
- def categorizeIntoClusters(matrix):
4530
- groups = []
4531
- while np.sum(matrix) > 0:
4532
- group = []
4533
- row = determineRow(matrix)
4534
- indexes = addIntoGroup(matrix, row)
4535
- groups.append(indexes)
4536
- matrix = deleteChosenRowsAndCols(matrix, indexes)
4537
- return groups
4538
-
4539
- def addIntoGroup(matrix, ind):
4540
- change = True
4541
- indexes = []
4542
- for col in range(len(matrix)):
4543
- if matrix[ind, col] == 1:
4544
- indexes.append(col)
4545
- while change == True:
4546
- change = False
4547
- numIndexes = len(indexes)
4548
- for i in indexes:
4549
- for col in range(len(matrix)):
4550
- if matrix[i, col] == 1:
4551
- if col not in indexes:
4552
- indexes.append(col)
4553
- numIndexes2 = len(indexes)
4554
- if numIndexes != numIndexes2:
4555
- change = True
4556
- return indexes
4557
-
4558
- def deleteChosenRowsAndCols(matrix, indexes):
4559
- for i in indexes:
4560
- matrix[i, :] = 0
4561
- matrix[:, i] = 0
4562
- return matrix
4563
- if not Topology.IsInstance(topology, "Topology"):
4564
- print("Topology.ClusterFaces - Error: the input topology parameter is not a valid topology. Returning None.")
4565
- return None
4566
- faces = []
4567
- _ = topology.Faces(None, faces)
4568
- normals = []
4569
- for aFace in faces:
4570
- normals.append(Face.Normal(aFace, outputType="XYZ", mantissa=3))
4571
- # build a matrix of similarity
4572
- mat = buildSimilarityMatrix(normals, angTolerance)
4573
- categories = categorizeIntoClusters(mat)
4574
- returnList = []
4575
- for aCategory in categories:
4576
- tempList = []
4577
- if len(aCategory) > 0:
4578
- for index in aCategory:
4579
- tempList.append(faces[index])
4580
- returnList.append(Topology.SelfMerge(Cluster.ByTopologies(tempList), tolerance=tolerance))
4581
- return returnList
4582
-
4583
4235
  @staticmethod
4584
4236
  def Contents(topology):
4585
4237
  """
@@ -6609,7 +6261,225 @@ class Topology():
6609
6261
  return None
6610
6262
  return topologic.Topology.IsSame(topologyA, topologyB)
6611
6263
 
6612
- def IsVertexCongruent(topologyA, topologyB, mantissa: int = 6, tolerance=0.0001, silent : bool = False):
6264
+ @staticmethod
6265
+ def IsSimilar(topologyA, topologyB, removeCoplanarFaces: bool = False, mantissa: int = 6, epsilon: float = 0.1, tolerance: float = 0.0001, silent: bool = False):
6266
+ """
6267
+ Returns True if the input topologies are similar. False otherwise. See https://en.wikipedia.org/wiki/Similarity_(geometry).
6268
+
6269
+ Parameters
6270
+ ----------
6271
+ topologyA : topologic_core.Topology
6272
+ The first input topology.
6273
+ topologyB : topologic_core.Topology
6274
+ The second input topology.
6275
+ removeCoplanarFaces : bool , optional
6276
+ If set to True, coplanar faces are removed. Otherwise they are not. The default is False.
6277
+ mantissa : int , optional
6278
+ The desired length of the mantissa. The default is 6.
6279
+ epsilon : float , optional
6280
+ The desired epsilon (another form of percentage tolerance) for finding if two objects are similar. Should be in the range 0 to 1. The default is 0.1 (10%).
6281
+ tolerance : float , optional
6282
+ The desired tolerance. The default is 0.0001.
6283
+ silent : bool , optional
6284
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
6285
+
6286
+ Returns
6287
+ -------
6288
+ [bool, list]
6289
+ True if the input topologies are similar., False otherwise and the matrix needed to tranform topologyA to match topologyB
6290
+
6291
+ """
6292
+ from topologicpy.Vertex import Vertex
6293
+ from topologicpy.Edge import Edge
6294
+ from topologicpy.Face import Face
6295
+ from topologicpy.Cell import Cell
6296
+ from topologicpy.Matrix import Matrix
6297
+ from topologicpy.Vector import Vector
6298
+ from topologicpy.Dictionary import Dictionary
6299
+
6300
+ if not Topology.IsInstance(topologyA, "topology"):
6301
+ if not silent:
6302
+ print("Topology.IsSimilar - Error: The input topologyA parameter is not a valid topology. Returning None.")
6303
+ return None
6304
+ if not Topology.IsInstance(topologyB, "topology"):
6305
+ if not silent:
6306
+ print("Topology.IsSimilar - Error: The input topologyB parameter is not a valid topology. Returning None.")
6307
+ return None
6308
+ if removeCoplanarFaces == True:
6309
+ topologyA = Topology.RemoveCoplanarFaces(topologyA, epsilon=epsilon, tolerance=tolerance)
6310
+ topologyB = Topology.RemoveCoplanarFaces(topologyB, epsilon=epsilon, tolerance=tolerance)
6311
+ Topology.Show(topologyA, topologyB)
6312
+ len_vertices_a = len(Topology.Vertices(topologyA))
6313
+ if len_vertices_a < 1 and not Topology.IsInstance(topologyA, "vertex"):
6314
+ if not silent:
6315
+ print("Topology.IsSimilar - Error: The input topologyA parameter is not a valid topology. Returning None.")
6316
+ return None
6317
+ len_vertices_b = len(Topology.Vertices(topologyB))
6318
+ if len_vertices_b < 1 and not Topology.IsInstance(topologyB, "vertex"):
6319
+ if not silent:
6320
+ print("Topology.IsSimilar - Error: The input topologyB parameter is not a valid topology. Returning None.")
6321
+ return None
6322
+ if not isinstance(epsilon, int) and not isinstance(epsilon, float):
6323
+ if not silent:
6324
+ print("Topology.IsSimilar - Error: The input epsilon parameter is not a valid float. Returning None.")
6325
+ return None
6326
+ if not (0 <= epsilon <= 1):
6327
+ if not silent:
6328
+ print("Topology.IsSimilar - Error: The input epsilon parameter is not within a valid range. Returning None.")
6329
+ return None
6330
+ # Trivial cases - All vertices are similar to each other.
6331
+ if Topology.IsInstance(topologyA, "vertex") and Topology.IsInstance(topologyB, "vertex"):
6332
+ centroid_a = Topology.Centroid(topologyA)
6333
+ centroid_b = Topology.Centroid(topologyB)
6334
+ trans_matrix_a = Matrix.ByTranslation(-Vertex.X(centroid_a), -Vertex.Y(centroid_a), -Vertex.Z(centroid_a))
6335
+ trans_matrix_b = Matrix.ByTranslation(Vertex.X(centroid_b), Vertex.Y(centroid_b), Vertex.Z(centroid_b))
6336
+ combined_matrix = Matrix.Multiply(trans_matrix_b, trans_matrix_a)
6337
+ return True, combined_matrix
6338
+
6339
+ # EXCLUSION TESTS
6340
+ # Topology Type
6341
+ if Topology.Type(topologyA) != Topology.Type(topologyB):
6342
+ return False, None
6343
+ # Number of vertices
6344
+ max_len = max([len_vertices_a, len_vertices_b])
6345
+ if abs(len_vertices_a - len_vertices_b)/max_len > epsilon:
6346
+ if not silent:
6347
+ print("Topology.IsSimilar - Info: Failed number of vertices check. Returning False.")
6348
+ return False, None
6349
+ # Number of edges
6350
+ len_edges_a = len(Topology.Edges(topologyA))
6351
+ len_edges_b = len(Topology.Edges(topologyB))
6352
+ max_len = max([len_edges_a, len_edges_b])
6353
+ if max_len > 0: # Check only if the topologies do actually have edges
6354
+ if abs(len_edges_a - len_edges_b)/max_len > epsilon:
6355
+ if not silent:
6356
+ print("Topology.IsSimilar - Info: Failed number of edges check. Returning False.")
6357
+ return False, None
6358
+ # Number of faces
6359
+ len_faces_a = len(Topology.Faces(topologyA))
6360
+ len_faces_b = len(Topology.Faces(topologyB))
6361
+ max_len = max([len_faces_a, len_faces_b])
6362
+ if max_len > 0: # Check only if the topologies do actually have faces
6363
+ if abs(len_faces_a - len_faces_b)/max_len > epsilon:
6364
+ if not silent:
6365
+ print("Topology.IsSimilar - Info: Failed number of faces check. Returning False.")
6366
+ return False, None
6367
+ # Number of cells
6368
+ len_cells_a = len(Topology.Cells(topologyA))
6369
+ len_cells_b = len(Topology.Cells(topologyB))
6370
+ max_len = max([len_cells_a, len_cells_b])
6371
+ if max_len > 0: # Check only if the topologies do actually have cells
6372
+ if abs(len_cells_a - len_cells_b)/max_len > epsilon:
6373
+ if not silent:
6374
+ print("Topology.IsSimilar - Info: Failed number of cells check. Returning False.")
6375
+ return False, None
6376
+ if Topology.IsInstance(topologyA, "face"):
6377
+ compactness_a = Face.Compactness(topologyA, mantissa=mantissa)
6378
+ compactness_b = Face.Compactness(topologyB, mantissa=mantissa)
6379
+ max_compactness = max([compactness_a, compactness_b])
6380
+ if max_compactness > 0: # Check only if the topologies do actually have compactness
6381
+ if abs(compactness_a - compactness_b)/max_compactness >= epsilon:
6382
+ if not silent:
6383
+ print("Topology.IsSimilar - Info: Failed compactness check. Returning False.")
6384
+ return False, None
6385
+ if Topology.IsInstance(topologyA, "cell"):
6386
+ compactness_a = Cell.Compactness(topologyA, mantissa=mantissa)
6387
+ compactness_b = Cell.Compactness(topologyB, mantissa=mantissa)
6388
+ max_compactness = max([compactness_a, compactness_b])
6389
+ if max_compactness > 0: # Check only if the topologies do actually have compactness
6390
+ if abs(compactness_a - compactness_b)/max_compactness > epsilon:
6391
+ if not silent:
6392
+ print("Topology.IsSimilar - Info: Failed compactness check. Returning False.")
6393
+ return False, None
6394
+ # Done with Exclusion Tests.
6395
+
6396
+ faces_a = Topology.Faces(topologyA)
6397
+ faces_b = Topology.Faces(topologyB)
6398
+ if len(faces_a) > 0 and len(faces_b) > 0:
6399
+ largest_faces_a = Topology.LargestFaces(topologyA)
6400
+ largest_faces_b = Topology.LargestFaces(topologyB)
6401
+
6402
+
6403
+ # Process largest faces
6404
+ largest_faces_a = Topology.LargestFaces(topologyA)
6405
+ largest_faces_b = Topology.LargestFaces(topologyB)
6406
+ face_a_d = Dictionary.ByKeyValue("faceColor", "red")
6407
+ face_b_d = Dictionary.ByKeyValue("faceColor", "blue")
6408
+ for face_a in largest_faces_a:
6409
+ centroid_a = Topology.Centroid(face_a)
6410
+ normal_a = Face.Normal(face_a)
6411
+ third_vertex_a = Face.ThirdVertex(face_a) # Pick a third vertex for orientation
6412
+ orientation_a = Vector.Normalize(Vector.ByVertices(centroid_a, third_vertex_a))
6413
+
6414
+ for face_b in largest_faces_b:
6415
+ face_copy_b = Topology.SetDictionary(Topology.Copy(face_b), face_b_d)
6416
+ centroid_b = Topology.Centroid(face_b)
6417
+ normal_b = Face.Normal(face_b)
6418
+ third_vertex_b = Face.ThirdVertex(face_b)
6419
+ orientation_b = Vector.Normalize(Vector.ByVertices(centroid_b, third_vertex_b))
6420
+
6421
+ # Compute transformation matrix
6422
+ rotation_matrix = Matrix.ByVectors(normal_a, normal_b, orientation_a, orientation_b)
6423
+ scaling_factor = (Face.Area(face_b) / Face.Area(face_a)) ** (1/2)
6424
+ scaling_matrix = Matrix.ByScaling(scaling_factor, scaling_factor, scaling_factor)
6425
+
6426
+ translation_matrix = Matrix.ByTranslation(
6427
+ Vertex.X(centroid_b) - Vertex.X(centroid_a),
6428
+ Vertex.Y(centroid_b) - Vertex.Y(centroid_a),
6429
+ Vertex.Z(centroid_b) - Vertex.Z(centroid_a)
6430
+ )
6431
+ combined_matrix = Matrix.Multiply(rotation_matrix, scaling_matrix)
6432
+ combined_matrix = Matrix.Multiply(translation_matrix, combined_matrix)
6433
+
6434
+ transformed_centroid_a = Topology.Transform(centroid_a, combined_matrix)
6435
+
6436
+ # One last translation to synchronise the two centroids.
6437
+ translation_matrix = Matrix.ByTranslation(
6438
+ Vertex.X(centroid_b) - Vertex.X(transformed_centroid_a),
6439
+ Vertex.Y(centroid_b) - Vertex.Y(transformed_centroid_a),
6440
+ Vertex.Z(centroid_b) - Vertex.Z(transformed_centroid_a)
6441
+ )
6442
+ combined_matrix = Matrix.Multiply(translation_matrix, combined_matrix)
6443
+ # Apply transformation and compare
6444
+ transformedA = Topology.Transform(topologyA, combined_matrix)
6445
+ transformed_face_a = Topology.Transform(face_a, combined_matrix)
6446
+ transformed_face_a = Topology.SetDictionary(face_a, face_a_d)
6447
+
6448
+ Topology.Show(transformedA, topologyB, transformed_face_a, face_copy_b, faceColorKey="faceColor", vertexSizeKey="vertexSize")
6449
+ if Topology.IsVertexCongruent(transformedA, topologyB, mantissa=mantissa, epsilon=epsilon, tolerance=tolerance, silent=silent):
6450
+ return True, combined_matrix
6451
+
6452
+ return False, None
6453
+ # for l_f_a in largest_faces_a:
6454
+ # l_e_a = Face.NormalEdge(l_f_a)
6455
+ # centroid_a = Edge.StartVertex(l_e_a)
6456
+ # dir_a = Vector.Normalize(Edge.Direction(l_e_a))
6457
+ # trans_matrix_a = Matrix.ByTranslation(-Vertex.X(centroid_a), -Vertex.Y(centroid_a), -Vertex.Z(centroid_a))
6458
+ # sf_a = 1/Face.Area(l_f_a)
6459
+ # scaling_matrix_a = Matrix.ByScaling(sf_a, sf_a, sf_a)
6460
+ # for l_f_b in largest_faces_b:
6461
+ # l_e_b = Face.NormalEdge(l_f_b)
6462
+ # centroid_b = Edge.StartVertex(l_e_b)
6463
+ # dir_b = Vector.Normalize(Edge.Direction(l_e_b))
6464
+ # rotation_matrix = Matrix.ByVectors(dir_a, dir_b)
6465
+ # sf_b = 1/Face.Area(l_f_b)
6466
+ # scaling_matrix_b = Matrix.ByScaling(sf_b, sf_b, sf_b)
6467
+ # trans_matrix_b = Matrix.ByTranslation(-Vertex.X(centroid_b), -Vertex.Y(centroid_b), -Vertex.Z(centroid_b))
6468
+ # combined_matrix_a = Matrix.Multiply(rotation_matrix, scaling_matrix_a)
6469
+ # combined_matrix_a = Matrix.Multiply(combined_matrix_a, trans_matrix_a)
6470
+ # combined_matrix_b = Matrix.Multiply(scaling_matrix_b, trans_matrix_b)
6471
+ # top_a = Topology.Transform(topologyA, combined_matrix_a)
6472
+ # top_b = Topology.Transform(topologyB, combined_matrix_b)
6473
+ # Topology.Show(top_a, top_b)
6474
+ # if Topology.IsVertexCongruent(top_a, top_b, mantissa=mantissa, epsilon=epsilon, tolerance=tolerance, silent=silent):
6475
+ # Topology.Show(top_a, top_b)
6476
+ # final_matrix = Matrix.Multiply(Matrix.Invert(combined_matrix_b), combined_matrix_a)
6477
+ # return True, final_matrix
6478
+
6479
+ return False, None
6480
+
6481
+ @staticmethod
6482
+ def IsVertexCongruent(topologyA, topologyB, mantissa: int = 6, epsilon: float = 0.1, tolerance: float = 0.0001, silent : bool = False):
6613
6483
  """
6614
6484
  Returns True if the input topologies are vertex matched (have same number of vertices and all vertices are congruent within a tolerance). Returns False otherwise.
6615
6485
 
@@ -6620,7 +6490,7 @@ class Topology():
6620
6490
  topologyB : topologic_core.Topology
6621
6491
  The second input topology.
6622
6492
  mantissa : int , optional
6623
- The desired length of the mantissa. The default is 6
6493
+ The desired length of the mantissa. The default is 6.
6624
6494
  tolerance : float , optional
6625
6495
  The desired tolerance. The default is 0.0001.
6626
6496
  silent : bool , optional
@@ -6634,10 +6504,10 @@ class Topology():
6634
6504
  """
6635
6505
  from topologicpy.Vertex import Vertex
6636
6506
 
6637
- def coordinates_match(list1, list2, tolerance):
6507
+ def coordinates_unmatched_ratio(list1, list2, tolerance):
6638
6508
  """
6639
- Checks if all coordinates in list1 have a corresponding coordinate in list2
6640
- within a specified tolerance, with each match being unique.
6509
+ Calculates the percentage of coordinates in list1 that do not have a corresponding
6510
+ coordinate in list2 within a specified tolerance, with each match being unique.
6641
6511
 
6642
6512
  Parameters
6643
6513
  ----------
@@ -6650,45 +6520,62 @@ class Topology():
6650
6520
 
6651
6521
  Returns
6652
6522
  -------
6653
- bool
6654
- True if all coordinates in list1 have a corresponding coordinate in list2
6655
- within the tolerance. False otherwise.
6523
+ float
6524
+ The percentage of coordinates in list1 that did not find a match in list2.
6656
6525
  """
6657
6526
  def distance(coord1, coord2):
6658
6527
  """Calculate the Euclidean distance between two coordinates."""
6659
6528
  return np.linalg.norm(np.array(coord1) - np.array(coord2))
6660
6529
 
6661
- # Copy list2 to keep track of unvisited coordinates
6530
+ unmatched_count = 0
6662
6531
  unmatched = list2.copy()
6663
6532
 
6664
6533
  for coord1 in list1:
6665
6534
  match_found = False
6666
6535
  for i, coord2 in enumerate(unmatched):
6667
6536
  if distance(coord1, coord2) <= tolerance:
6668
- # Mark the coordinate as visited by removing it
6669
- unmatched.pop(i)
6537
+ unmatched.pop(i) # Remove matched coordinate
6670
6538
  match_found = True
6671
6539
  break
6672
6540
  if not match_found:
6673
- return False
6674
- return True
6541
+ unmatched_count += 1
6542
+
6543
+ total_coordinates = len(list1)
6544
+ unmatched_ratio = (unmatched_count / total_coordinates)
6545
+
6546
+ return unmatched_ratio
6675
6547
 
6676
6548
  if not Topology.IsInstance(topologyA, "topology"):
6677
6549
  if not silent:
6678
- print("Topology.IsVertexMatched - Error: The input topologyA parameter is not a valid topology. Returning None.")
6550
+ print("Topology.IsVertexCongruent - Error: The input topologyA parameter is not a valid topology. Returning None.")
6679
6551
  return None
6680
6552
  if not Topology.IsInstance(topologyB, "topology"):
6681
6553
  if not silent:
6682
- print("Topology.IsVertexMatched - Error: The input topologyB parameter is not a valid topology. Returning None.")
6554
+ print("Topology.IsVertexCongruent - Error: The input topologyB parameter is not a valid topology. Returning None.")
6683
6555
  return None
6684
6556
 
6685
6557
  vertices_a = Topology.Vertices(topologyA)
6558
+ len_vertices_a = len(vertices_a)
6559
+ if len_vertices_a < 1:
6560
+ if not silent:
6561
+ print("Topology.IsVertexCongruent - Error: The input topologyA parameter is not a valid topology. Returning None.")
6562
+ return None
6686
6563
  vertices_b = Topology.Vertices(topologyB)
6687
- if not len(vertices_a) == len(vertices_b):
6564
+ len_vertices_b = len(vertices_b)
6565
+ if len_vertices_b < 1:
6566
+ if not silent:
6567
+ print("Topology.IsVertexCongruent - Error: The input topologyB parameter is not a valid topology. Returning None.")
6568
+ return None
6569
+ # Number of vertices
6570
+ max_len = max([len_vertices_a, len_vertices_b])
6571
+ if abs(len_vertices_a - len_vertices_a)/max_len >= epsilon:
6688
6572
  return False
6689
6573
  coords_a = [Vertex.Coordinates(v, mantissa=mantissa) for v in vertices_a]
6690
6574
  coords_b = [Vertex.Coordinates(v, mantissa=mantissa) for v in vertices_b]
6691
- return coordinates_match(coords_a, coords_b, tolerance=tolerance)
6575
+ unmatched_ratio = coordinates_unmatched_ratio(coords_a, coords_b, tolerance=tolerance)
6576
+ if unmatched_ratio <= epsilon:
6577
+ return True
6578
+ return False
6692
6579
 
6693
6580
  @staticmethod
6694
6581
  def LargestFaces(topology, removeCoplanarFaces: bool = False, epsilon: float = 0.001, tolerance: float = 0.0001, silent: bool = False):
@@ -6779,6 +6666,7 @@ class Topology():
6779
6666
  if removeCoplanarFaces == True:
6780
6667
  simple_topology = Topology.RemoveCoplanarFaces(topology)
6781
6668
  simple_topology = Topology.RemoveCollinearEdges(simple_topology)
6669
+
6782
6670
  edges = Topology.Edges(simple_topology)
6783
6671
  edge_lengths = [Edge.Length(edge) for edge in edges]
6784
6672
  max_length = max(edge_lengths)
@@ -9666,7 +9554,7 @@ class Topology():
9666
9554
 
9667
9555
 
9668
9556
  @staticmethod
9669
- def Transform(topology, matrix):
9557
+ def Transform(topology, matrix: list, silent: bool = False):
9670
9558
  """
9671
9559
  Transforms the input topology by the input 4X4 transformation matrix.
9672
9560
 
@@ -9676,6 +9564,8 @@ class Topology():
9676
9564
  The input topology.
9677
9565
  matrix : list
9678
9566
  The input 4x4 transformation matrix.
9567
+ silent : bool , optional
9568
+ If set to True, no warnings or errors will be printed. The default is False.
9679
9569
 
9680
9570
  Returns
9681
9571
  -------
@@ -9683,6 +9573,10 @@ class Topology():
9683
9573
  The transformed topology.
9684
9574
 
9685
9575
  """
9576
+ from topologicpy.Vertex import Vertex
9577
+
9578
+ import numpy as np
9579
+
9686
9580
  kTranslationX = 0.0
9687
9581
  kTranslationY = 0.0
9688
9582
  kTranslationZ = 0.0
@@ -9710,7 +9604,52 @@ class Topology():
9710
9604
  kTranslationY = matrix[1][3]
9711
9605
  kTranslationZ = matrix[2][3]
9712
9606
 
9713
- return_topology = topologic.TopologyUtility.Transform(topology, kTranslationX, kTranslationY, kTranslationZ, kRotation11, kRotation12, kRotation13, kRotation21, kRotation22, kRotation23, kRotation31, kRotation32, kRotation33)
9607
+ if not Topology.IsInstance(topology, "topology"):
9608
+ if not silent:
9609
+ print("Topology.Trasnform - Error: The input topology parameter is not a valid topology. Returning None.")
9610
+ return None
9611
+
9612
+ try:
9613
+ return_topology = topologic.TopologyUtility.Transform(topology, kTranslationX, kTranslationY, kTranslationZ, kRotation11, kRotation12, kRotation13, kRotation21, kRotation22, kRotation23, kRotation31, kRotation32, kRotation33)
9614
+ except:
9615
+ print("topologic.TopologyUtility.Transform failed. Attempting a workaround.")
9616
+
9617
+ # Extract translation (last column of the matrix)
9618
+ translation = [m[3] for m in matrix[:3]]
9619
+ print("translation", translation)
9620
+ x_translate, y_translate, z_translate = translation
9621
+
9622
+ # Extract rotation (top-left 3x3 part of the matrix)
9623
+ rotation_matrix = [m[:3] for m in matrix[:3]]
9624
+
9625
+ # Extract scaling (diagonal of the matrix)
9626
+ scaling_factors = [matrix[m][m] for m in [0,1,2]] # scaling is stored in the diagonal of the rotation matrix
9627
+ print(scaling_factors)
9628
+ x_scale, y_scale, z_scale = scaling_factors
9629
+
9630
+ # Step 1: Apply Scaling
9631
+ # Here, origin is assumed to be (0,0,0) for simplicity
9632
+ return_topology = Topology.Scale(topology, origin=Vertex.ByCoordinates(0, 0, 0), x=x_scale, y=y_scale, z=z_scale)
9633
+
9634
+ # Step 2: Apply Rotation
9635
+ # The rotation axis and angle need to be derived from the rotation matrix
9636
+ # For simplicity, we assume the matrix represents a standard rotation (angle and axis).
9637
+ # The angle can be computed from the matrix as the arccos of the trace.
9638
+
9639
+ angle_rad = np.arccos((np.trace(rotation_matrix) - 1) / 2.0) # Using trace to compute angle
9640
+ axis = np.array([rotation_matrix[2, 1] - rotation_matrix[1, 2],
9641
+ rotation_matrix[0, 2] - rotation_matrix[2, 0],
9642
+ rotation_matrix[1, 0] - rotation_matrix[0, 1]])
9643
+ axis = axis / np.linalg.norm(axis) # Normalize the axis
9644
+
9645
+ # Convert the angle from radians to degrees
9646
+ angle_deg = np.degrees(angle_rad)
9647
+
9648
+ # Apply rotation
9649
+ return_topology = Topology.Rotate(return_topology, origin=Vertex.ByCoordinates(0, 0, 0), axis=axis, angle=angle_deg)
9650
+
9651
+ # Step 3: Apply Translation
9652
+ return_topology = Topology.Translate(return_topology, x=x_translate, y=y_translate, z=z_translate)
9714
9653
 
9715
9654
  vertices = Topology.Vertices(topology)
9716
9655
  edges = Topology.Edges(topology)
@@ -9747,7 +9686,7 @@ class Topology():
9747
9686
  return return_topology
9748
9687
 
9749
9688
  @staticmethod
9750
- def Translate(topology, x=0, y=0, z=0):
9689
+ def Translate(topology, x=0, y=0, z=0, silent: bool = False):
9751
9690
  """
9752
9691
  Translates (moves) the input topology.
9753
9692
 
@@ -9761,6 +9700,8 @@ class Topology():
9761
9700
  The y translation value. The default is 0.
9762
9701
  z : float , optional
9763
9702
  The z translation value. The default is 0.
9703
+ silent : bool , optional
9704
+ If set to True, no warnings or errors will be printed. The default is False.
9764
9705
 
9765
9706
  Returns
9766
9707
  -------
@@ -9772,7 +9713,8 @@ class Topology():
9772
9713
  from topologicpy.Dictionary import Dictionary
9773
9714
 
9774
9715
  if not Topology.IsInstance(topology, "Topology"):
9775
- print("Topology.Translate - Error: The input topology parameter is not a valid topology. Returning None.")
9716
+ if not silent:
9717
+ print("Topology.Translate - Error: The input topology parameter is not a valid topology. Returning None.")
9776
9718
  return None
9777
9719
 
9778
9720
  if Topology.IsInstance(topology, "vertex"):
@@ -9791,7 +9733,9 @@ class Topology():
9791
9733
  try:
9792
9734
  return_topology = topologic.TopologyUtility.Translate(topology, x, y, z)
9793
9735
  except:
9794
- return_topology = topology
9736
+ if not silent:
9737
+ print("Topology.Translate - Error: The operation failed. Returning the original input.")
9738
+ return topology
9795
9739
 
9796
9740
  r_vertices = Topology.Vertices(return_topology)
9797
9741
  r_edges = Topology.Edges(return_topology)
@@ -9965,7 +9909,7 @@ class Topology():
9965
9909
  return topology.Type()
9966
9910
 
9967
9911
  @staticmethod
9968
- def TypeAsString(topology, silent=False):
9912
+ def TypeAsString(topology, silent: bool = False):
9969
9913
  """
9970
9914
  Returns the type of the input topology as a string.
9971
9915