-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
1965 lines (1669 loc) · 84.3 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
from enum import Enum
import math
import random
from collections import deque
import numpy as np
from typing import TypeVar, Any
import sys
from PyQt6.QtGui import QDoubleValidator
from PyQt6.QtWidgets import (
QMainWindow, QApplication,
QComboBox, QVBoxLayout, QWidget, QLabel, QCheckBox, QSizePolicy, QRadioButton, QLineEdit, QFileDialog, QPushButton
)
from PyQt6.QtCore import Qt
import time
try:
import threading
USE_THREADING = True
except ModuleNotFoundError:
USE_THREADING = False
print("WARNING: Module 'threading' not found.",
"The program will still run but Blender will crash upon exiting the GUI")
try:
import bpy
import bmesh
except ModuleNotFoundError:
raise Exception("BPY or BMESH not found: This program must be run in Blender in order to work")
try:
import dill
USE_DILL = True
USE_PICKLE = False
except ModuleNotFoundError:
try:
import pickle
USE_PICKLE = True
USE_DILL = False
print("WARNING: Module 'dill' not found. Using 'pickle' instead.")
except ModuleNotFoundError:
USE_PICKLE = False
USE_DILL = False
print("WARNING: Modules 'dill' and 'pickle' not found. Saving capability will be disabled.")
try:
from PyNite.Visualization import Renderer
from PyNite.FEModel3D import FEModel3D
USE_PYNITE = True
except ModuleNotFoundError:
USE_PYNITE = False
print("WARNING: Module 'PyNite' not found. Model analysis capability will be disabled.")
VectorType = TypeVar("VectorType", bound="VectorTup")
MaterialType = TypeVar("MaterialType", bound="Material")
ForceObjType = TypeVar("ForceObjType", bound="ForceObject")
ForceVertType = TypeVar("ForceVertType", bound="ForceVertex")
BlendObjectType = TypeVar("BlendObjectType", bound="BlendObject")
# A vector, representable as a tuple
class VectorTup:
def __init__(self, x: float = 0, y: float = 0, z: float = 0) -> None:
self.x = x
self.y = y
self.z = z
def __mul__(self, other: float | VectorType) -> VectorType: # Other int / float
if isinstance(other, float):
return VectorTup(self.x * other, self.y * other, self.z * other)
else:
return VectorTup(self.x * other.x, self.y * other.y, self.z * other.z)
def __rmul__(self, other: float | VectorType) -> VectorType:
if isinstance(other, float):
return VectorTup(self.x * other, self.y * other, self.z * other)
else:
return VectorTup(self.x * other.x, self.y * other.y, self.z * other.z)
def __truediv__(self, other: float) -> VectorType:
return VectorTup(self.x / other, self.y / other, self.z / other)
def __add__(self, other: VectorType) -> VectorType:
return VectorTup(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other: VectorType) -> VectorType:
return VectorTup(self.x - other.x, self.y - other.y, self.z - other.z)
def __repr__(self) -> str:
return f"Vector: ({self.x}, {self.y}, {self.z})"
def __str__(self) -> str:
return f"({self.x}, {self.y}, {self.z})"
def __bool__(self) -> bool:
return not (self.x or self.y or self.z) # If all numbers are 0 or invalid return false (via de morgan's laws)
def __neg__(self) -> VectorType:
return VectorTup(-self.x, -self.y, -self.z)
def __lt__(self, other: VectorType) -> bool: # Comparator definitions
return self.get_magnitude() < other.get_magnitude()
def __gt__(self, other: VectorType) -> bool:
return self.get_magnitude() > other.get_magnitude()
def __le__(self, other: VectorType) -> bool:
return self.get_magnitude() <= other.get_magnitude()
def __ge__(self, other: VectorType) -> bool:
return self.get_magnitude() >= other.get_magnitude()
def __ne__(self, other: VectorType) -> bool: # Tests for inequality of entire vector, not magnitude inequality
return not (self.x == other.x and self.y == other.y and self.z == other.z)
def __eq__(self, other: VectorType) -> bool:
return self.x == other.x and self.y == other.y and self.z == other.z
def __getitem__(self, key: int) -> float:
if key == 0:
return self.x
elif key == 1:
return self.y
elif key == 2:
return self.z
else:
raise IndexError("VectorTup: Index out of bounds")
def __setitem__(self, key: int, value: float) -> None:
if key == 0:
self.x = value
elif key == 1:
self.y = value
elif key == 2:
self.z = value
else:
raise IndexError("VectorTup: Index out of bounds")
def __getstate__(self) -> dict:
return {"x": self.x, "y": self.y, "z": self.z}
def __setstate__(self, state: dict) -> None:
self.x, self.y, self.z = state["x"], state["y"], state["z"]
def normalise(self) -> None:
""" Calculates normalised version of vector without returning
:return: None
"""
magnitude = math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
self.x = self.x / magnitude
self.y = self.y / magnitude
self.z = self.z / magnitude
def get_normalised(self) -> VectorType:
""" Calculates and returns normalised version of vector
:return: Normalised vector
"""
magnitude = math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
x_temp = self.x / magnitude
y_temp = self.y / magnitude
z_temp = self.z / magnitude
return VectorTup(x_temp, y_temp, z_temp)
def cross(self, other: VectorType) -> VectorType:
return VectorTup(self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x)
def set_magnitude(self, magnitude: int) -> None:
""" Normalises vector then multiplies it by given magnitude
:param magnitude: Magnitude to set vector to
:return: None
"""
ini_magnitude = math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
self.x = (self.x / ini_magnitude) * magnitude
self.y = (self.y / ini_magnitude) * magnitude
self.z = (self.z / ini_magnitude) * magnitude
def get_magnitude(self) -> float:
return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
def as_tup(self) -> tuple[float, float, float]:
return self.x, self.y, self.z
class Material:
def __init__(self, name: str, density: float, E: float, G: float, rad: float = 10) -> None:
"""These parameters are all specific named properties of a material
'Members' refers to the edges from vertex to vertex in the object
:param name: String
:param density: Float: Density of elements, used to calculate their mass
:unit: Kg/M3
:param E: Float: Modulus of elasticity of material members
:unit: Pascals (N / M2)
:param G: Float: Shear modulus of material members
:unit: Pascals (N / M2)
:param rad: Float: Radius of elements, treated as circles
:unit: Meters
:Iy: Float: Moment of inertia of material's members about their local y-axis
:Iz: Float: Moment of inertia of material's members about their local z-axis
:J: Float: Polar moment of inertia of the material's members
:A: Float: Cross-sectional area of material's members (Internal beam areas)
"""
self.name = name
self.density = density
self.E = E
self.G = G
self.Iy = (math.pi * (rad ** 4)) / 4
self.Iz = 2 * self.Iy
self.J = (math.pi * ((2 * rad) ** 4)) / 32
self.A = math.pi * (rad ** 2)
def __repr__(self) -> str:
return f"Material: {self.name} [{self.density}, {self.E}, {self.G}, {self.Iy}, {self.Iz}, {self.J}, {self.A}]"
def __str__(self) -> str:
return f"""{self.name}
[Density: {self.density}, E: {self.E}, G: {self.G}, Iy: {self.Iy}, Iz: {self.Iz}, J: {self.J}, A: {self.A}]"""
def __getitem__(self, key) -> float:
if key == 0 or key.lower() == "density":
return self.density
elif key == 1 or key == "E":
return self.E
elif key == 2 or key == "G":
return self.G
elif key == 3 or key == "Iy":
return self.Iy
elif key == 4 or key == "Iz":
return self.Iz
elif key == 5 or key == "J":
return self.J
elif key == 6 or key == "A":
return self.A
raise Exception("Invalid key: Material")
def __setitem__(self, key, value: float) -> None:
if key == 0 or key.lower() == "density":
self.density = value
elif key == 1 or key == "E":
self.E = value
elif key == 2 or key == "G":
self.G = value
elif key == 3 or key == "Iy":
self.Iy = value
elif key == 4 or key == "Iz":
self.Iz = value
elif key == 5 or key == "J":
self.J = value
elif key == 6 or key == "A":
self.A = value
raise Exception("Invalid Key: Material")
def __len__(self) -> int:
return 7
def __getstate__(self) -> dict:
return {"density": self.density, "E": self.E,
"G": self.G, "Iy": self.Iy,
"Iz": self.Iz, "J": self.J,
"A": self.A}
def __setstate__(self, state: dict) -> None:
self.density = state["density"]
self.E = state["E"]
self.G = state["G"]
self.Iy = state["Iy"]
self.Iz = state["Iz"]
self.J = state["J"]
self.A = state["A"]
def as_tup(self) -> tuple[float, float, float, float, float, float, float]:
return self.density, self.E, self.G, self.Iy, self.Iz, self.J, self.A
def recalc_radius(self, rad: float = 0.01) -> None:
self.Iy = (math.pi * (rad ** 4)) / 4
self.Iz = 2 * self.Iy
self.J = (math.pi * ((2 * rad) ** 4)) / 32
self.A = math.pi * (rad ** 2)
@staticmethod
def return_recalc_radius(rad: float) -> tuple[float, float, float, float]:
"""
:param rad: radius of object formed with material
:return: (Iy, Iz, J, A)
"""
return (math.pi * (rad ** 4)) / 4, (math.pi * (rad ** 4)) / 2, \
(math.pi * ((2 * rad) ** 4)) / 32, math.pi * (rad ** 2)
class MaterialEnum(Enum):
"""
Enum class containing pre-definitions for materials to be used
Material format is: Name, Density, Modulus of Elasticity, Shear Modulus
"""
# Steel material from:
# https://www.metalformingmagazine.com/article/?/materials/high-strength-steel/metal-properties-elastic-modulus
STEEL = Material("STEEL", 7900, 2.1e11, 7.93e10)
# Birch material from: https://www.matweb.com/search/datasheet_print.aspx?matguid=c499c231f20d4284a4da8bea3d2644fc
WOOD_BIRCH = Material("WOOD_BIRCH", 640, 1.186e10, 8.34e6)
# Oak material from: https://www.matweb.com/search/DataSheet.aspx?MatGUID=ea505704d8d343f2800de42db7b16de8&ckck=1
# Green oak specifically
WOOD_OAK = Material("WOOD_OAK", 750, 7.86e9, 6.41e6)
# Granite material from: https://www.matweb.com/search/datasheet.aspx?matguid=3d4056a86e79481cb6a80c89caae1d90
# and https://www.sciencedirect.com/science/article/pii/S1674775522000993
GRANITE = Material("GRANITE", 1463, 4e10, 6.1e7)
# Diamond material from: http://www.chm.bris.ac.uk/motm/diamond/diamprop.htm
# and https://arxiv.org/ftp/arxiv/papers/1811/1811.09503.pdf
DIAMOND = Material("DIAMOND", 3340, 1.22e12, 5.3e11)
# Plastic material from: https://designerdata.nl/materials/plastics/thermo-plastics/low-density-polyethylene
PLASTIC_POLYETHYLENE = Material("PLASTIC_POLYETHYLENE", 955, 3e8, 2.25e8)
# Plastic material from: https://www.matweb.com/search/datasheet_print.aspx?matguid=e19bc7065d1c4836a89d41ff23d47413
PLASTIC_PVC = Material("PLASTIC_PVC", 1300, 1.7e9, 6.35e7)
# Glass material from: https://www.structuralglass.org/single-post/2016/11/26/glass-physical-properties
# The model cannot account for the drastic difference in tensile and compressive strength for glass
# As such glass simulation will be unrealistic for tension, but correct for compression
GLASS = Material("GLASS", 2500, 7e10, 2.8e9)
# Copper material from:http://www.mit.edu/~6.777/matprops/copper.htm
# and https://www.azom.com/properties.aspx?ArticleID=597
COPPER = Material("COPPER", 8960, 1.84e10, 6.74e9)
# Aluminium material from: https://www.britannica.com/science/shear-modulus
# and https://www.mit.edu/~6.777/matprops/aluminum.htm
ALUMINIUM = Material("ALUMINIUM", 2700, 7e10, 2.4e10)
# Brass material from: https://www.matweb.com/search/datasheet_print.aspx?matguid=d3bd4617903543ada92f4c101c2a20e5
BRASS = Material("BRASS", 8890, 9.84e10, 3.55e10)
# Lightweight concrete
# Concrete material from: https://civiltoday.com/civil-engineering-materials/concrete/361-density-of-concrete
# https://www.fhwa.dot.gov/publications/research/infrastructure/pavements/05062/chapt2c.cfm
CONCRETE_LIGHT = Material("CONCRETE_LIGHT", 2000, 2.3e10, 1e10)
# Dense concrete
# Concrete material from: https://www.fhwa.dot.gov/publications/research/infrastructure/pavements/05062/chapt2c.cfm
CONCRETE_DENSE = Material("CONCRETE_DENSE", 2400, 3.28e10, 1.43e10)
# C90/105 Reinforced concrete
# This concrete material will act more accurately than the other two
# as it's compressive and tensile strengths are more similar than regular concrete
# where tensile is lower than compressive normally
# Concrete material from: https://eurocodeapplied.com/design/en1992/concrete-design-properties
CONCRETE_REINFORCED = Material("CONCRETE_REINFORCED", 2400, 4.36e10, 1.82e10)
# Object populated with edges
class ForceObject:
def __init__(self, verts: list[ForceVertType],
edges: list[list[int]], density: float) -> None:
"""
:param verts: List[ForceVertex]
:param edges: List[List[Int]] : Inner list of len 2
:param density: float : kilograms / m^2
"""
self.verts = verts
self.edges = edges
self.density = density
self.mass = 1
edgewise_mass = 1 / len(edges)
self.edge_masses = [edgewise_mass] * len(edges)
self.edge_rads = [10] * len(edges)
self.base_nodes = self.find_base(tolerance=0.4)
print(f"Force Object Initialised: {len(self.verts)} Verts, {len(self.edges)} Edges")
def __repr__(self) -> str:
return f"ForceObject: ({len(self.verts)} Verts) ({len(self.edges)} Edges)"
def __str__(self) -> str:
temp = ""
i = 0
for edge in self.edges:
temp += f"{i} : "
temp += [edge[0], edge[1]].__str__()
temp += "\n"
i += 1
return temp
def __len__(self) -> int:
return len(self.verts)
def __getstate__(self) -> dict:
return {"verts": [v.__getstate__() for v in self.verts],
"edges": self.edges,
"mass": self.mass}
def __setstate__(self, state: dict) -> None:
self.verts = [VectorTup(v["x"], v["y"], v["z"]) for v in state["verts"]]
self.edges = state["edges"]
self.edges = state["mass"]
def apply_random_forces(self, frange: tuple[float]) -> None: # Tuple [2] specifying min and max values
for vert in self.verts:
temp_vec = make_random_vector(frange)
vert.dir += temp_vec
def apply_gravity(self) -> None:
"""
Applies gravitational force to object vertex-wise based on the formula F = GMm/r2
:return: None
"""
grav_constant = 6.67e-11 # Newton's gravitational constant, with unit Nm2kg-2
earth_mass = 5.972e24 # Mass of the earth in kg
earth_rad = 6.371e6 # Average radius of the earth in m
inverse_rad = 1 / (earth_rad ** 2) # Compute division outside loop for performance reasons
for vert_nums, element_mass in zip(self.edges, self.edge_masses):
# The calculated force must be halved as it is assumed to be evenly split over both vertices
gravitational_force = VectorTup(0, 0, - (grav_constant * earth_mass * element_mass * inverse_rad * 0.5))
self.verts[vert_nums[0]].dir += gravitational_force
self.verts[vert_nums[1]].dir += gravitational_force
def apply_field_forces(self, min_vert: VectorType, max_vert: VectorType, force: VectorType) -> None:
"""
Applies forces generated by a force field to a force object's vertices
:param min_vert: Vertex with minimum x,y and z values from the force field
:param max_vert: Vertex with the maximum x,y and z values from the force field
:param force: x, y and z components of the force applied by the force field
:return: None
"""
for vert in self.verts:
if min_vert.x < vert.loc.x < max_vert.x and min_vert.y < vert.loc.y < max_vert.y and min_vert.z < vert.loc.z < max_vert.z:
vert.dir += force
def apply_force_fields(self, field_list: list) -> None:
for force_field in field_list:
min_x = math.inf
min_y = math.inf
min_z = math.inf
max_x = -math.inf
max_y = -math.inf
max_z = -math.inf
for force_vert in force_field.data.vertices:
temp_glob = force_field.matrix_world @ force_vert.co
min_x = min(min_x, temp_glob[0])
min_y = min(min_y, temp_glob[1])
min_z = min(min_z, temp_glob[2])
max_x = max(max_x, temp_glob[0])
max_y = max(max_y, temp_glob[1])
max_z = max(max_z, temp_glob[2])
force_x = force_field.get("FORCE_STRENGTH_X")
force_y = force_field.get("FORCE_STRENGTH_Y")
force_z = force_field.get("FORCE_STRENGTH_Z")
self.apply_field_forces(
VectorTup(min_x, min_y, min_z),
VectorTup(max_x, max_y, max_z),
VectorTup(force_x, force_y, force_z)
)
def find_base(self, tolerance: float = 0) -> set[int]:
""" Finds nodes which the ForceObject would rest on if placed vertically downwards
:param tolerance: The tolerance range of nodes that sum up the base
:return: a set of nodes which represents the base of the object, a set is used for ensuring uniqueness
"""
min_height: float = math.inf
base_nodes = set()
for i, vert in enumerate(self.verts):
if vert.loc.z + tolerance < min_height:
min_height = vert.loc.z
base_nodes = {i}
elif vert.loc.z - tolerance <= min_height <= vert.loc.z + tolerance:
base_nodes.add(i)
return base_nodes
# Creates n links from each vertex in object 1 to vertices in object two
def mesh_link(self, other: ForceObjType, num_links: int = 2) -> None:
""" Does not interact with object faces
:param other: ForceObject
:param num_links: Int: Defines how many links are created from each vertex
:return: None
"""
extracted = self.verts
other_extracted = other.verts
shift = len(extracted)
# Shifts the indices of object two by the length of object one
# This is done such that the index of mesh two's first vertex is n+1 for n = length of mesh one
new_edges = deque([]) # Deque to contain pairs of vertices representing new edges to be made
# Deque used here as the only way new_edges will be modified is by appending, which is faster for a deque
if num_links < 1: # Forces a positive amount of links
num_links = 1
for i, vert in enumerate(extracted):
# Iterates through each vertex from the first object
min_dist = [math.inf] * num_links # Creates a list to carry currently found smallest distances
temp_closest_nums = deque([None] * num_links) # Creates a deque to carry indices of found closest vertices
# The deque data structure is used here as it is optimised for adding and removing left and rightmost vals
for j, vert2 in enumerate(other_extracted):
# Iterates through each vertex from the other object
temp_dist = vert.get_euclidean_distance(vert2)
# Gets euclidean distance between initial and second vert
min_dist, flag = min_add(min_dist, temp_dist)
# Adds current distance to min_dist list in the correct position if it is less than any list values
# flag boolean indicates whether a new distance has been added to min_dist
if flag:
temp_closest_nums.appendleft(j + shift)
temp_closest_nums.pop()
# If a new smallest distance has been found, performs both:
# Removes largest distance edge of the temp_closest_nums list
# Adds new small distance edge to temp_closest_nums list
if None not in temp_closest_nums:
# Checks if appropriate closest nodes have been found
# This will only fail if len(other_extracted) < num_links
for vtc in temp_closest_nums:
# Iterates through the closest vertices that have been found for current vertex
new_edges.append([i, vtc]) # Adds an edge from node i to node vtc
else:
print("WARNING: ForceObject.mesh_link failed, meshes will not be linked")
return
self.verts.extend(other_extracted)
# Adds second object's vertices to list of current object's vertices
self.edges.extend(new_edges)
# Adds newly formed edges to current object's edges
self.edges.extend([[edge_new[0] + shift, edge_new[1] + shift] for edge_new in other.edges])
# Adds second object's edges to first object
# This operation shifts each edge value to point to the correct verts in the new combined object
def mesh_link_chain(self, others: list[ForceObjType], num_links: int = 2) -> None:
"""Creates n links from each vertex of every object to vertices in other objects in the list
Does not interact with object faces
:param others: List[ForceObject]
:param num_links: Int : Defines how many links are created from each vertex
:return: None
"""
MAX_DIST = math.inf # Very large constant
extracted = [self.verts] # List containing lists of each object's vertices
shifts = [0, len(extracted[0])]
# List to shift the indices of all objects by the length of all objects that come before
# This is done such that the index of mesh m first vertex is n+1 for n = length of combined previous meshes
new_edges = deque([]) # Deque to contain pairs of vertices representing new edges to be made
# Deque used here as the only way new_edges will be modified is by appending, which is faster for a deque
for item in others: # Iterate through other objects
extracted.append(item.verts) # Adds vertex information of each other object to extracted
shifts.append(len(extracted[-1]) + shifts[-1]) # Adds new shifts for object m
for mesh_num, active_mesh in enumerate(extracted):
# Iterates over each mesh to check for its closest links in all other meshes
for vert_num, active_vert in enumerate(active_mesh):
# Iterates over each vertex in active mesh
min_dist = [MAX_DIST] * num_links # Creates a list to carry currently found smallest distances
closest_indices = deque([None] * num_links) # Creates a deque to hold indices of found closest vertices
# Deque is used here as it is optimised for adding and removing left and rightmost vals
for secondary_mesh_num in (n for n in range(len(extracted)) if n != mesh_num):
# Iterates through all meshes other than active mesh
# tuple comprehension used for the iterator for its performance over list
for secondary_vert_num, secondary_vert in enumerate(extracted[secondary_mesh_num]):
# Iterates through all vertices in secondary mesh
temp_dist = active_vert.get_euclidean_distance(secondary_vert)
# Gets euclidean distance between initial and second vert
min_dist, flag = min_add(min_dist, temp_dist)
# Inserts current distance to min_dist list if it is less than any list values
# flag boolean indicates whether a new distance has been added to min_dist
if flag:
closest_indices.appendleft(secondary_vert_num + shifts[secondary_mesh_num])
closest_indices.pop()
# If a new smallest distance has been found, performs both:
# Removes largest distance edge of the closest_indices list
# Adds new small distance edge to closest_indices list
for final_ind in closest_indices:
# Loops through closest indices found for the active mesh
if [final_ind, vert_num + shifts[mesh_num]] not in new_edges:
# Checks if the reverse of edge has been already added to the edge list
new_edges.append([vert_num + shifts[mesh_num], final_ind])
# Adds edge from node vert_num with the correct shift to final_ind to the new_edges list
self.edges.extend(new_edges)
# Adds newly formed edges to current object's edges
for i, other in enumerate(others):
# Loops through all other objects and adds their information to the current object
self.verts.extend(other.verts)
# Adds other object's vertices to list of current object's vertices
self.edges.extend([[new_edge[0] + shifts[i + 1], new_edge[1] + shifts[i + 1]] for new_edge in other.edges])
# Adds other object's edges to first object
# This operation shifts each edge value to point to the correct verts in the new combined object
def node_collide(self, edge: list[int], bpy_material_objects: list[tuple[VectorType, VectorType]]) -> int:
for i, vert_pair in enumerate(bpy_material_objects):
if (vert_pair[0].x < self.verts[edge[0]].loc.x < vert_pair[1].x and
vert_pair[0].y < self.verts[edge[0]].loc.y < vert_pair[1].y and
vert_pair[0].z < self.verts[edge[0]].loc.z < vert_pair[1].z):
return i
return -1
# Creates a finite element model from a mesh
def to_finite(self, mat: MaterialType, lock_dict: dict, spring_constant: float) -> FEModel3D: # Redo return typing
""" Compiles ForceObject to FEA model via the following steps:
- Loads in each node from the ForceObject
- Adds each edge from the ForceObject
- Adds each nodal force on the ForceObject
- Adds spring supports
- Adds standard supports
:param mat: Material Object: Self defined material object, not blender material
:param lock_dict: Dictionary containing directional lock parameters for each normal node
:param spring_constant: Spring constant of supporting base springs
:return: FEModel3D Object
"""
final_finite = FEModel3D()
# Unpacks base material
base_density, base_E, base_G, base_Iy, base_Iz, base_J, base_A = mat.as_tup()
truth_dict = {
"support_DX": True,
"support_DY": True,
"support_DZ": True,
"support_RX": True,
"support_RY": True,
"support_RZ": True,
}
# Gets the raw blender material objects
bpy_objects_material = [obj for obj in bpy.data.objects if obj.get("MATERIAL") is not None]
mat_field_coords = []
mat_field_materials = []
# Unpacks bpy objects into two arrays
# One array contains the defining coordinates for each obj (i.e. lowest value and highest value vert)
# The other contains the material representations of each object defined by their "MATERIAL" property
for mat_field in bpy_objects_material:
min_x, min_y, min_z = math.inf, math.inf, math.inf
max_x, max_y, max_z = -math.inf, -math.inf, -math.inf
# Finds minimum and maximum vertex locations
for force_vert in mat_field.data.vertices:
temp_glob = mat_field.matrix_world @ force_vert.co
min_x = min(min_x, temp_glob[0])
min_y = min(min_y, temp_glob[1])
min_z = min(min_z, temp_glob[2])
max_x = max(max_x, temp_glob[0])
max_y = max(max_y, temp_glob[1])
max_z = max(max_z, temp_glob[2])
# Adds min and max coordinates in VectorTup objects to a list to avoid unnecessary re-computing
mat_field_coords.append((VectorTup(min_x, min_y, min_z), VectorTup(max_x, max_y, max_z)))
# Populates material list with actual material values by using mat_field key references
mat_field_materials.append(MaterialEnum[mat_field["MATERIAL"]].value)
for i, node in enumerate(self.verts):
final_finite.add_node(str(i), node.loc.x, node.loc.y, node.loc.z)
if i not in self.base_nodes:
final_finite.def_support(
str(i),
**lock_dict
)
else:
# Adds springs to the base nodes
spring_node_name_compression = f"{i}Cs"
spring_node_name_tension = f"{i}Ts"
final_finite.add_node(spring_node_name_compression, node.loc.x, node.loc.y, node.loc.z - 1.1)
final_finite.add_node(spring_node_name_tension, node.loc.x, node.loc.y, node.loc.z - 1)
# Adds node from which spring can be linked to corresponding base node
final_finite.add_spring(
f"SpringC{i}",
str(i),
spring_node_name_compression,
spring_constant,
tension_only=False,
comp_only=True
)
final_finite.add_spring(
f"SpringT{i}",
str(i),
spring_node_name_tension,
spring_constant,
tension_only=True,
comp_only=False
)
final_finite.def_support(
spring_node_name_compression,
**truth_dict
) # Supports spring base node in all directions
final_finite.def_support(
spring_node_name_tension,
**truth_dict
) # Supports spring base node in all directions
# Alternates compression only and tension only springs to avoid model instability
# Adds supports to the base nodes
final_finite.def_support(str(i), support_DX=True, support_DY=True, support_RZ=True)
for j, (edge, rad) in enumerate(zip(self.edges, self.edge_rads)):
# Checks if both nodes in edge collide with each matrix field
# Uses walrus operator to automatically assign the index if it is true
if (temp_mat_index := self.node_collide(edge, mat_field_coords)) != -1:
# These temp variables exist so regular density, E and G values are not overwritten
( # This looks insane but PEP8 dictates it must be so
temp_density, temp_E, temp_G,
temp_Iy, temp_Iz, temp_J, temp_A
) = mat_field_materials[temp_mat_index].as_tup()
temp_Iy, temp_Iz, temp_J, temp_A = mat_field_materials[temp_mat_index].return_recalc_radius(rad)
final_finite.add_member(
f"Edge{j}",
str(edge[0]),
str(edge[1]),
temp_E,
temp_G,
temp_Iy,
temp_Iz,
temp_J,
temp_A
)
else:
base_Iy, base_Iz, base_J, base_A = mat.return_recalc_radius(rad)
final_finite.add_member(
f"Edge{j}",
str(edge[0]),
str(edge[1]),
base_E,
base_G,
base_Iy,
base_Iz,
base_J,
base_A
)
for k, fnode in enumerate(self.verts):
final_finite.add_node_load(str(k), Direction='FX', P=fnode.dir.x,
case="Case 1")
final_finite.add_node_load(str(k), Direction='FY', P=fnode.dir.y,
case="Case 1")
final_finite.add_node_load(str(k), Direction='FZ', P=fnode.dir.z,
case="Case 1")
# self.base_nodes is a list of indices of vertices within self.verts
# where the nodes comprise the base of the object
return final_finite
def get_net_moment(self) -> VectorType:
"""
:return: VectorTup : (Moment X, Moment Y, Moment Z)
:unit: NewtonMeters (Possibly NewtonInches depending on blender)
"""
COG: VectorType = self.get_centre_of_gravity()
final = VectorTup()
for vert in self.verts:
dist = COG - vert.loc
final += dist.cross(vert.dir)
return final
def get_centre_of_gravity(self) -> VectorType:
""" Gets the centre of gravity of an object,
assumes uniform mass distribution,
uses vertex locations as mass points
:return: VectorTup : (x,y,z)
"""
final = VectorTup()
for vert in self.verts:
final += vert.loc
final /= len(self.verts)
return final
def to_blend_object(self) -> BlendObjectType:
return BlendObject(self.verts, self.edges)
@staticmethod
def get_edge_mass(length: float, radius: float, density: float) -> float:
""" Calculates and returns the "mass" of an edge E.
Calculated as if the edge were a cylinder.
:param length: length of cylinder
:param radius: pre-defined cylinder radius
:param density: cylinder material density
:unit: Kg/M3
:return: calculated mass via visible formula
"""
return math.pi * (radius ** 2) * length * density
@staticmethod
def get_edge_rad(length: float, mass: float, density: float) -> float:
""" Gets radius of cylindrical representation of an edge
Must be given its length, mass and density
:param length: length of cylinder
:param mass: cylinder mass
:param density: cylinder material density
:unit: Kg/M3
:return: calculated radius based on formula
"""
if length == 0 or density == 0: # Prevents divide by zero error
return 0
return math.sqrt(mass / (length * density * math.pi))
class ForceObjectUniformMass(ForceObject):
def __init__(self, verts: list[ForceVertType], edges: list[list[int]], density: float, mass: float):
""" Subclass of ForceObject which enforces uniform masses along each edge of the object
This uniform mass means that the radii of each edge can vary given uniform density
Each edge is treated as a cylinder
:param verts: List of vertices
:param edges: List of edges (pairs of vertex indices)
:param density: Density of the material the object is made out of
:param mass: Enforced mass of overall object, must be divided by edge number for edgewise mass
"""
super().__init__(verts, edges, density)
average_edge_mass = mass / len(edges) # Divides overall mass for edgewise mass
self.edge_masses = [average_edge_mass] * len(self.edges)
# Gets a list of lengths of each edge (distances between point A and B in edge (A,B))
distances = [self.verts[edge[0]].get_euclidean_distance(self.verts[edge[1]]) for edge in self.edges]
# Gets the radii of each edge given fixed mass and the length of each edge
self.edge_rads = [self.get_edge_rad(dist, average_edge_mass, density) for dist in distances]
self.mass = mass
class ForceObjectUniformRad(ForceObject):
def __init__(self, verts: list[ForceVertType], edges: list[list[int]], density: float, radius: float):
""" Subclass of ForceObject which enforces uniform "radius" of each edge represented as a cylinder
This uniform edge radius means that each edge gets to have different mass
This also allows us to calculate the overall mass of the object given the information we have
:param verts: List of vertices
:param edges: List of edges (pairs of vertex indices)
:param density: Density of the material the object is made out of
:param radius: Enforced radius of cylindrical representation of each edge in object
"""
super().__init__(verts, edges, density)
self.edge_rads = [radius] * len(edges)
# Gets a list of lengths of each edge (distances between point A and B in edge (A,B))
distances = [self.verts[edge[0]].get_euclidean_distance(self.verts[edge[1]]) for edge in self.edges]
# Gets the masses of each edge given fixed radius
self.edge_masses = [self.get_edge_mass(dist, radius, density) for dist in distances]
# Overall mass is sum of calculated edge masses
self.mass = sum(self.edge_masses)
class ForceVertex:
def __init__(self, loc: VectorType, direction: VectorType) -> None:
self.loc: VectorType = loc
self.dir: VectorType = direction
def __repr__(self) -> str:
return f"ForceVertex: (loc:{self.loc}, dir:{self.dir})"
def __str__(self) -> str:
return f"(loc:{self.loc}, dir:{self.dir})"
def __add__(self, other: VectorType | ForceVertType) -> ForceVertType:
if isinstance(other, VectorTup):
return ForceVertex(self.loc, self.dir + other)
elif isinstance(other, ForceVertex):
return ForceVertex(self.loc, self.dir + other.dir)
else:
raise TypeError(f"Invalid ForceVertex addition: ForceVertex, {type(other)}")
def __sub__(self, other: VectorType | ForceVertType) -> ForceVertType:
if isinstance(other, VectorTup):
return ForceVertex(self.loc, self.dir - other)
elif isinstance(other, ForceVertex):
return ForceVertex(self.loc, self.dir - other.dir)
else:
raise TypeError(f"Invalid ForceVertex addition: ForceVertex, {type(other)}")
def __getstate__(self) -> dict:
return {"loc": self.loc.__getstate__(),
"dir": self.dir.__getstate__()}
def __setstate__(self, state) -> None:
self.loc = VectorTup(state["loc"]["x"], state["loc"]["y"], state["loc"]["z"])
self.dir = VectorTup(state["dir"]["x"], state["dir"]["y"], state["dir"]["z"])
def get_magnitude(self) -> float:
return self.dir.get_magnitude()
def apply_force(self, force: VectorType) -> None:
self.dir = self.dir + force
def get_euclidean_distance(self, other: ForceVertType) -> float:
return math.dist(self.loc.as_tup(), other.loc.as_tup())
# Representation of a blender object to be rendered in the scene
class BlendObject:
def __init__(self, verts: list[ForceVertType], edges: list[list[int]]) -> None:
self.verts = [vert.loc for vert in verts]
self.forces = [vert_force.dir for vert_force in verts]
self.edges = edges # Make sure these are of form bpy.context.object.data.edges
self.materials = []
for i in range(0, 5, 1):
self.materials.append(create_new_shader(str(i), (i, 0, 5 - i, 1)))
def __getstate__(self) -> dict:
return {"name": self.name,
"verts": [v.__getstate__() for v in self.verts],
"forces": [f.__getstate__() for f in self.forces],
"edges": self.edges}
def __setstate__(self, state: dict) -> None:
self.name = state["name"]
self.verts = [VectorTup(v["x"], v["y"], v["z"]) for v in state["verts"]]
self.forces = [VectorTup(f["x"], f["y"], f["z"]) for f in state["forces"]]
self.edges = state["edges"]
self.materials = []
for i in range(0, 5, 1): # Re-defines class materials from scratch as these cannot natively be pickled
self.materials.append(create_new_shader(str(i), (i, 0, 5 - i, 1)))
def make(self, fast: bool = False) -> None:
"""
:param fast: boolean : Determines whether the fast rendering method is used
Fast Rendering: Renders 0 dimensional mesh of edges and vertices
Slow Rendering: Renders coloured edges based on the forces applied
:return: None
"""
if fast: # https://blender.stackexchange.com/questions/100913/render-large-3d-graphs-millions-of-vertices-edges
bpy.ops.mesh.primitive_cube_add() # Creates a primitive cube so context object is at (0, 0, 0)
context_dat = bpy.context.object.data
bm = bmesh.new()
vert_map = {}
# Adds each vertex to a bmesh, transposes data to more efficient
for i, pos in enumerate(self.verts):
pos = np.array(pos.as_tup())
vert = bm.verts.new(pos)
vert_map[i] = vert
for edge in self.edges:
bm.edges.new((vert_map[edge[0]], vert_map[edge[1]]))
bm.to_mesh(context_dat)
bm.free()
else:
for edge in self.edges:
self.create_cylinder((self.verts[edge[0]], self.verts[edge[1]]), 0.01)
# Adapted from:
# https://blender.stackexchange.com/questions/5898/how-can-i-create-a-cylinder-linking-two-points-with-python
def create_cylinder(self, points: tuple[tuple[float]], cylinder_radius: float) -> None:
""" Creates a cylinder
Y axis has to be entirely flipped due to some earlier error which I will not be addressing yet
:param points: list[Tuple[float]] : list of length 2 containing a start and end point for the given cylinder
:param cylinder_radius: float : radius of created cylinder
:return: None
"""
x_dist = points[1][0] - points[0][0]
y_dist = points[0][1] - points[1][1]
z_dist = points[1][2] - points[0][2]
distance = math.sqrt(x_dist ** 2 + y_dist ** 2 + z_dist ** 2)
if distance == 0:
return
bpy.ops.mesh.primitive_cylinder_add(
radius=cylinder_radius,
depth=distance,
location=(x_dist / 2 + points[0][0],
y_dist / 2 + points[1][1],
z_dist / 2 + points[0][2]),
vertices=3
)
phi_rotation = math.atan2(x_dist, y_dist)
theta_rotation = math.acos(z_dist / distance)
bpy.context.object.rotation_euler[0] = theta_rotation
bpy.context.object.rotation_euler[2] = phi_rotation
bpy.context.object.data.materials.append(random.choice(self.materials))
class MaterialForceWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("BlendiForce Menu")
self.setStyleSheet("""
QCheckBox {
font-size: 15pt;
}
QCheckBox::indicator {
width: 13px;
height: 13px;
}