topologicpy 0.8.26__py3-none-any.whl → 0.8.29__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.
@@ -0,0 +1,492 @@
1
+ # Copyright (C) 2025
2
+ # Wassim Jabi <wassim.jabi@gmail.com>
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it under
5
+ # the terms of the GNU Affero General Public License as published by the Free Software
6
+ # Foundation, either version 3 of the License, or (at your option) any later
7
+ # version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful, but WITHOUT
10
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12
+ # details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License along with
15
+ # this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import topologic_core as topologic
18
+
19
+ class ShapeGrammar:
20
+ def __init__(self):
21
+ self.title = "Untitled" # Stores the title of the topology grammar.
22
+ self.description = "" # Stores the description of the grammar.
23
+ self.rules = [] # Stores transformation rules of the topology grammar.
24
+ # Operations
25
+ # Replace
26
+ replace = {"title": "Replace",
27
+ "description": "Replaces the input topology with the output topology.",
28
+ "uSides": None,
29
+ "vSides": None,
30
+ "wSides": None}
31
+ # Transform
32
+ transform = {"title": "Transform",
33
+ "description": "Transforms the input topology using the specified matrix.",
34
+ "uSides": None,
35
+ "vSides": None,
36
+ "wSides": None}
37
+ # Union
38
+ union = {"title": "Union",
39
+ "description": "Unions the input topology and the output topology.",
40
+ "uSides": None,
41
+ "vSides": None,
42
+ "wSides": None}
43
+ # Difference
44
+ difference = {"title": "Difference",
45
+ "description": "Subtracts the output topology from the input topology.",
46
+ "uSides": None,
47
+ "vSides": None,
48
+ "wSides": None}
49
+ # Difference
50
+ symdif = {"title": "Symmetric Difference",
51
+ "description": "Calculates the symmetrical difference of the input topology and the output topology.",
52
+ "uSides": None,
53
+ "vSides": None,
54
+ "wSides": None}
55
+ # Intersect
56
+ intersect = {"title": "Intersect",
57
+ "description": "Intersects the input topology and the output topology.",
58
+ "uSides": None,
59
+ "vSides": None,
60
+ "wSides": None}
61
+ # Merge
62
+ merge = {"title": "Merge",
63
+ "description": "Merges the input topology and the output topology.",
64
+ "uSides": None,
65
+ "vSides": None,
66
+ "wSides": None}
67
+ # Slice
68
+ slice = {"title": "Slice",
69
+ "description": "Slices the input topology using the output topology.",
70
+ "uSides": None,
71
+ "vSides": None,
72
+ "wSides": None}
73
+ # Impose
74
+ impose = {"title": "Impose",
75
+ "description": "Imposes the output topology on the input topology.",
76
+ "uSides": None,
77
+ "vSides": None,
78
+ "wSides": None}
79
+ # Imprint
80
+ imprint = {"title": "Imprint",
81
+ "description": "Imposes the output topology on the input topology.",
82
+ "uSides": None,
83
+ "vSides": None,
84
+ "wSides": None}
85
+ # Divide
86
+ divide = {"title": "Divide",
87
+ "description": "Divides the input topology along the x, y, and z axes using the specified number of sides (uSides, vSides, wSides)",
88
+ "uSides": 2,
89
+ "vSides": 2,
90
+ "wSides": 2}
91
+ self.operations = [replace, transform, union, difference, symdif, intersect, merge, slice, impose, imprint, divide]
92
+
93
+ def OperationTitles(self):
94
+ """
95
+ Returns the list of available operation titles.
96
+
97
+ Parameters
98
+ ----------
99
+
100
+ Returns
101
+ -------
102
+ list
103
+ The requested list of operation titles
104
+ """
105
+ return [op["title"] for op in self.operations]
106
+
107
+ def OperationByTitle(self, title):
108
+ """
109
+ Returns the operation given the input title string
110
+
111
+ Parameters
112
+ ----------
113
+ title : str
114
+ The input operation str. See OperationTitles for list of operations.
115
+
116
+ Returns
117
+ -------
118
+ ShapeGrammar.Operation
119
+ The requested operation
120
+ """
121
+ for op in self.operations:
122
+ op_title = op["title"]
123
+ if title.lower() in op_title.lower():
124
+ return op
125
+ return None
126
+
127
+ def AddRule(self,
128
+ input,
129
+ output,
130
+ title : str = "Untitled Rule",
131
+ description: str = "",
132
+ operation : dict = None,
133
+ matrix: list = None,
134
+ silent: bool = False):
135
+ """
136
+ Adds a rule to the topology grammar.
137
+
138
+ Parameters
139
+ ----------
140
+ input : topologic_core.Topology
141
+ The linput topology of the rule.
142
+ output : topologic_core.Topology
143
+ The output topology of the rule.
144
+ title : str , optional
145
+ The title of the rule. The default is "Untitled Rule"
146
+ description : str, optional
147
+ The description of the rule. The default is "".
148
+ operation : dict , optional
149
+ The desired rule operation. See Rule Operations. If set to None, the replacement rule is applied. The default is None.
150
+ matrix : list
151
+ The 4x4 transformation matrix that tranforms the output topology to the input topology. If set to None, no transformation is applied. The default is None.
152
+ silent : bool, optional
153
+ If True, suppresses error/warning messages. Default is False.
154
+
155
+ Returns
156
+ -------
157
+ None
158
+ This method does not return a value.
159
+ """
160
+ from topologicpy.Topology import Topology
161
+
162
+ def is_4x4_matrix(matrix):
163
+ return (
164
+ isinstance(matrix, list) and
165
+ len(matrix) == 4 and
166
+ all(isinstance(row, list) and len(row) == 4 for row in matrix)
167
+ )
168
+
169
+ if not Topology.IsInstance(input, "Topology"):
170
+ if not silent:
171
+ print("ShapeGrammar.AddRule - Error: The input input parameter is not a valid topology. Returning None.")
172
+ return None
173
+ if not output == None:
174
+ if not Topology.IsInstance(output, "Topology"):
175
+ if not silent:
176
+ print("ShapeGrammar.AddRule - Error: The input output parameter is not a valid topology. Returning None.")
177
+ return None
178
+ if not operation == None:
179
+ if not operation in self.operations:
180
+ if not silent:
181
+ print("ShapeGrammar.AddRule - Error: The input operation parameter is not a valid operation. Returning None.")
182
+ return None
183
+ if not matrix == None:
184
+ if not is_4x4_matrix(matrix):
185
+ if not silent:
186
+ print("ShapeGrammar.AddRule - Error: The input matrix parameter is not a valid matrix. Returning None.")
187
+ return None
188
+
189
+ self.rules.append({"input":input,
190
+ "output": output,
191
+ "title": title,
192
+ "description": description,
193
+ "operation": operation,
194
+ "matrix": matrix
195
+ })
196
+
197
+ def ApplicableRules(self, topology, keys: list = None, silent: bool = False):
198
+ """
199
+ Returns rules applicable to the input topology.
200
+
201
+ Parameters
202
+ ----------
203
+ topology : topologic_core.Topology
204
+ The input topology
205
+ keys : list , optional
206
+ The list of dictionary keys to semantically match the rules. The default is None which means dictionaries are not considered.
207
+ silent : bool, optional
208
+ If True, suppresses error/warning messages. Default is False.
209
+
210
+ Returns
211
+ -------
212
+ list
213
+ The list of applicable rules.
214
+ """
215
+ from topologicpy.Topology import Topology
216
+ from topologicpy.Dictionary import Dictionary
217
+
218
+ if not Topology.IsInstance(topology, "Topology"):
219
+ if not silent:
220
+ print("ShapeGrammar.ApplicableRules - Error: The input topology parameter is not a valid topology. Returning None.")
221
+ return None
222
+
223
+ ap_rules = []
224
+ ap_trans = []
225
+ d = Topology.Dictionary(topology)
226
+ for i, rule in enumerate(self.rules):
227
+ dict_status = True
228
+ input = rule["input"]
229
+ # If there is a list of keys specified, check that the values match
230
+ if isinstance(keys, list):
231
+ d_input = Topology.Dictionary(input)
232
+ for j, key in enumerate(keys):
233
+ if not Dictionary.ValueAtKey(d, key, None) == Dictionary.ValueAtKey(d_input, key, None):
234
+ dict_status = False
235
+ break
236
+ #If it passed the dictionary key test, then check topology similarity
237
+ if dict_status:
238
+ topology_status, mat = Topology.IsSimilar(rule["input"], topology)
239
+ if topology_status:
240
+ ap_rules.append(rule)
241
+ ap_trans.append(mat)
242
+ return ap_rules, ap_trans
243
+
244
+ def ApplyRule(self, topology, rule: dict = None, matrix: list = None, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
245
+ """
246
+ Returns rules applicable to the input topology.
247
+
248
+ Parameters
249
+ ----------
250
+ topology : topologic_core.Topology
251
+ The input topology
252
+ rule : dict , optional
253
+ The desired rule to apply. The default is None.
254
+ matrix : list
255
+ The 4x4 transformation matrix that tranforms the output topology to the input topology. If set to None, no transformation is applied. The default is None.
256
+ mantissa : int, optional
257
+ Decimal precision. Default is 6.
258
+ tolerance : float, optional
259
+ The desired Tolerance. Not used here but included for API compatibility. Default is 0.0001.
260
+ silent : bool, optional
261
+ If True, suppresses error/warning messages. Default is False.
262
+
263
+ Returns
264
+ -------
265
+ topologic_core.Topology
266
+ The transformed topology
267
+ """
268
+
269
+ from topologicpy.Topology import Topology
270
+ from topologicpy.Cluster import Cluster
271
+ from topologicpy.Face import Face
272
+ from topologicpy.Vertex import Vertex
273
+
274
+ def is_4x4_matrix(matrix):
275
+ return (
276
+ isinstance(matrix, list) and
277
+ len(matrix) == 4 and
278
+ all(isinstance(row, list) and len(row) == 4 for row in matrix)
279
+ )
280
+
281
+ def bb(topology):
282
+ vertices = Topology.Vertices(topology)
283
+ x = []
284
+ y = []
285
+ z = []
286
+ for aVertex in vertices:
287
+ x.append(Vertex.X(aVertex, mantissa=mantissa))
288
+ y.append(Vertex.Y(aVertex, mantissa=mantissa))
289
+ z.append(Vertex.Z(aVertex, mantissa=mantissa))
290
+ x_min = min(x)
291
+ y_min = min(y)
292
+ z_min = min(z)
293
+ maxX = max(x)
294
+ maxY = max(y)
295
+ maxZ = max(z)
296
+ return [x_min, y_min, z_min, maxX, maxY, maxZ]
297
+
298
+ def slice(topology, uSides, vSides, wSides):
299
+ x_min, y_min, z_min, maxX, maxY, maxZ = bb(topology)
300
+ centroid = Vertex.ByCoordinates(x_min+(maxX-x_min)*0.5, y_min+(maxY-y_min)*0.5, z_min+(maxZ-z_min)*0.5)
301
+ wOrigin = Vertex.ByCoordinates(Vertex.X(centroid, mantissa=mantissa), Vertex.Y(centroid, mantissa=mantissa), z_min)
302
+ wFace = Face.Rectangle(origin=wOrigin, width=(maxX-x_min)*1.1, length=(maxY-y_min)*1.1)
303
+ wFaces = []
304
+ wOffset = (maxZ-z_min)/wSides
305
+ for i in range(wSides-1):
306
+ wFaces.append(Topology.Translate(wFace, 0,0,wOffset*(i+1)))
307
+ uOrigin = Vertex.ByCoordinates(x_min, Vertex.Y(centroid, mantissa=mantissa), Vertex.Z(centroid, mantissa=mantissa))
308
+ uFace = Face.Rectangle(origin=uOrigin, width=(maxZ-z_min)*1.1, length=(maxY-y_min)*1.1, direction=[1,0,0])
309
+ uFaces = []
310
+ uOffset = (maxX-x_min)/uSides
311
+ for i in range(uSides-1):
312
+ uFaces.append(Topology.Translate(uFace, uOffset*(i+1),0,0))
313
+ vOrigin = Vertex.ByCoordinates(Vertex.X(centroid, mantissa=mantissa), y_min, Vertex.Z(centroid, mantissa=mantissa))
314
+ vFace = Face.Rectangle(origin=vOrigin, width=(maxX-x_min)*1.1, length=(maxZ-z_min)*1.1, direction=[0,1,0])
315
+ vFaces = []
316
+ vOffset = (maxY-y_min)/vSides
317
+ for i in range(vSides-1):
318
+ vFaces.append(Topology.Translate(vFace, 0,vOffset*(i+1),0))
319
+ all_faces = uFaces+vFaces+wFaces
320
+ if len(all_faces) > 0:
321
+ f_clus = Cluster.ByTopologies(uFaces+vFaces+wFaces)
322
+ return Topology.Slice(topology, f_clus, tolerance=tolerance)
323
+ else:
324
+ return topology
325
+
326
+ if not Topology.IsInstance(topology, "Topology"):
327
+ if not silent:
328
+ print("ShapeGrammar.ApplyRule - Error: The input topology parameter is not a valid topology. Returning None.")
329
+ return None
330
+ if not matrix == None:
331
+ if not is_4x4_matrix(matrix):
332
+ if not silent:
333
+ print("ShapeGrammar.ApplyRule - Error: The input matrix parameter is not a valid matrix. Returning None.")
334
+ return None
335
+
336
+ if not rule == None:
337
+ input = rule["input"]
338
+ output = rule["output"]
339
+ r_matrix = rule["matrix"]
340
+ operation = rule["operation"]
341
+ if not operation == None:
342
+ op_title = operation["title"]
343
+ else:
344
+ op_title = "None"
345
+
346
+ result_output = topology
347
+ temp_output = None
348
+ if not output == None:
349
+ temp_output = output
350
+ # Transform the output topology to the input topology to prepare it for final transformation
351
+ if not r_matrix == None and not output == None:
352
+ temp_output = Topology.Transform(output, r_matrix)
353
+
354
+ if "replace" in op_title.lower():
355
+ result_output = temp_output
356
+ elif "transform" in op_title.lower():
357
+ result_output = Topology.Transform(topology, r_matrix)
358
+ elif "union" in op_title.lower():
359
+ result_output = Topology.Union(input, temp_output)
360
+ elif "difference" in op_title.lower():
361
+ result_output = Topology.Difference(input, temp_output)
362
+ elif "symmetric difference" in op_title.lower():
363
+ result_output = Topology.SymmetricDifference(input, temp_output)
364
+ elif "intersect" in op_title.lower():
365
+ result_output = Topology.Intersect(input, temp_output)
366
+ elif "merge" in op_title.lower():
367
+ result_output = Topology.Merge(input, temp_output)
368
+ elif "slice" in op_title.lower():
369
+ result_output = Topology.Slice(input, temp_output)
370
+ elif "impose" in op_title.lower():
371
+ result_output = Topology.Impose(input, temp_output)
372
+ elif "imprint" in op_title.lower():
373
+ result_output = Topology.Imprint(input, temp_output)
374
+ elif "divide" in op_title.lower():
375
+ uSides = operation["uSides"]
376
+ vSides = operation["vSides"]
377
+ wSides = operation["wSides"]
378
+ if not uSides == None and not vSides == None and not wSides == None:
379
+ result_output = slice(input, uSides, vSides, wSides)
380
+
381
+ # Finally, transform the result to the input topology
382
+ if not matrix == None:
383
+ result_output = Topology.Transform(result_output, matrix)
384
+
385
+ return result_output
386
+
387
+ def FigureByInputOutput(self, input, output, silent: bool = False):
388
+ """
389
+ Returns the Plotly figure of the input and output topologies as a rule.
390
+
391
+ Parameters
392
+ ----------
393
+ input : topologic_core.Topology
394
+ The input topology
395
+ output : topologic_core.Topology
396
+ The output topology
397
+ silent : bool, optional
398
+ If True, suppresses error/warning messages. Default is False.
399
+
400
+ Returns
401
+ -------
402
+ This function does not return a value
403
+ """
404
+
405
+ from topologicpy.Vertex import Vertex
406
+ from topologicpy.Cell import Cell
407
+ from topologicpy.Topology import Topology
408
+ from topologicpy.Dictionary import Dictionary
409
+ from topologicpy.Cluster import Cluster
410
+ from topologicpy.Plotly import Plotly
411
+
412
+ if not Topology.IsInstance(input, "Topology"):
413
+ if not silent:
414
+ print("ShapeGrammar.DrawInputOutput - Error: The input topology parameter is not a valid topology. Returning None.")
415
+ return None
416
+ if not Topology.IsInstance(output, "Topology"):
417
+ if not silent:
418
+ print("ShapeGrammar.DrawInputOutput - Error: The output topology parameter is not a valid topology. Returning None.")
419
+ return None
420
+
421
+ input_bb = Topology.BoundingBox(input)
422
+ input_centroid = Topology.Centroid(input_bb)
423
+ input_d = Topology.Dictionary(input_bb)
424
+ xmin = Dictionary.ValueAtKey(input_d, "xmin")
425
+ ymin = Dictionary.ValueAtKey(input_d, "ymin")
426
+ zmin = Dictionary.ValueAtKey(input_d, "zmin")
427
+ xmax = Dictionary.ValueAtKey(input_d, "xmax")
428
+ ymax = Dictionary.ValueAtKey(input_d, "ymax")
429
+ zmax = Dictionary.ValueAtKey(input_d, "zmax")
430
+ input_width = xmax-xmin
431
+ input_length = ymax-ymin
432
+ input_height = zmax-zmin
433
+ input_max = max(input_width, input_length, input_height)
434
+ sf = 1/input_max
435
+ temp_input = Topology.Translate(input, -Vertex.X(input_centroid), -Vertex.Y(input_centroid), -Vertex.Z(input_centroid))
436
+ temp_input = Topology.Scale(temp_input, x=sf, y=sf, z=sf)
437
+ temp_input = Topology.Translate(temp_input, 0.5, 0, 0)
438
+
439
+ output_bb = Topology.BoundingBox(output)
440
+ output_centroid = Topology.Centroid(output_bb)
441
+ output_d = Topology.Dictionary(output_bb)
442
+ xmin = Dictionary.ValueAtKey(output_d, "xmin")
443
+ ymin = Dictionary.ValueAtKey(output_d, "ymin")
444
+ zmin = Dictionary.ValueAtKey(output_d, "zmin")
445
+ xmax = Dictionary.ValueAtKey(output_d, "xmax")
446
+ ymax = Dictionary.ValueAtKey(output_d, "ymax")
447
+ zmax = Dictionary.ValueAtKey(output_d, "zmax")
448
+ output_width = xmax-xmin
449
+ output_length = ymax-ymin
450
+ output_height = zmax-zmin
451
+ output_max = max(output_width, output_length, output_height)
452
+ sf = 1/output_max
453
+ temp_output = Topology.Translate(output, -Vertex.X(output_centroid), -Vertex.Y(output_centroid), -Vertex.Z(output_centroid))
454
+ temp_output = Topology.Scale(temp_output, x=sf, y=sf, z=sf)
455
+ temp_output = Topology.Translate(temp_output, 2.5, 0, 0)
456
+
457
+ cyl = Cell.Cylinder(radius=0.04, height=0.4, placement="bottom")
458
+ cyl=Topology.Rotate(cyl, axis=[0,1,0], angle=90)
459
+ cyl = Topology.Translate(cyl, 1.25, 0, 0)
460
+
461
+ cone = Cell.Cone(baseRadius=0.1, topRadius=0, height=0.15, placement="bottom")
462
+ cone=Topology.Rotate(cone, axis=[0,1,0], angle=90)
463
+ cone = Topology.Translate(cone, 1.65, 0, 0)
464
+ cluster = Cluster.ByTopologies([temp_input, temp_output, cyl, cone])
465
+ cluster = Topology.Place(cluster, originA=Topology.Centroid(cluster), originB=Vertex.Origin())
466
+ data = Plotly.DataByTopology(cluster)
467
+ fig = Plotly.FigureByData(data)
468
+ return fig
469
+
470
+ def FigureByRule(self, rule, silent: bool = False):
471
+ """
472
+ Returns the Plotly figure of the input rule.
473
+
474
+ Parameters
475
+ ----------
476
+ rule : dict
477
+ The input rule
478
+ silent : bool, optional
479
+ If True, suppresses error/warning messages. Default is False.
480
+
481
+ Returns
482
+ -------
483
+ This function does not return a value
484
+ """
485
+ from topologicpy.Topology import Topology
486
+ if not isinstance(rule, dict):
487
+ if not silent:
488
+ print("ShapeGrammar.DrawRule - Error: The input rule parameter is not a valid rule. Returning None.")
489
+ return None
490
+ input = rule["input"]
491
+ output = self.ApplyRule(input, rule)
492
+ return self.FigureByInputOutput(input, output)
topologicpy/Shell.py CHANGED
@@ -1846,6 +1846,42 @@ class Shell():
1846
1846
  final_result = Shell.ByFaces(final_faces, tolerance=tolerance)
1847
1847
  return final_result
1848
1848
 
1849
+ @staticmethod
1850
+ def Square(origin= None, size: float = 1.0,
1851
+ uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1],
1852
+ placement: str = "center", tolerance: float = 0.0001):
1853
+ """
1854
+ Creates a square.
1855
+
1856
+ Parameters
1857
+ ----------
1858
+ origin : topologic_core.Vertex , optional
1859
+ The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0).
1860
+ size : float , optional
1861
+ The size of the square. The default is 1.0.
1862
+ length : float , optional
1863
+ The length of the square. The default is 1.0.
1864
+ uSides : int , optional
1865
+ The number of sides along the width. The default is 2.
1866
+ vSides : int , optional
1867
+ The number of sides along the length. The default is 2.
1868
+ direction : list , optional
1869
+ The vector representing the up direction of the square. The default is [0, 0, 1].
1870
+ placement : str , optional
1871
+ The description of the placement of the origin of the square. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1872
+ tolerance : float , optional
1873
+ The desired tolerance. The default is 0.0001.
1874
+
1875
+ Returns
1876
+ -------
1877
+ topologic_core.Shell
1878
+ The created shell square.
1879
+
1880
+ """
1881
+ return Shell.Rectangle(origin=origin, width=size, length=size,
1882
+ uSides=uSides, vSides=vSides, direction=direction,
1883
+ placement=placement, tolerance=tolerance)
1884
+
1849
1885
  @staticmethod
1850
1886
  def Vertices(shell) -> list:
1851
1887
  """