Cleanup: trailing space
[blender-addons.git] / add_mesh_extra_objects / add_mesh_pipe_joint.py
blob52103992e899715cb8f91878448dda89b737c6c2
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Author: Buerbaum Martin (Pontiac)
5 import bpy, bmesh
6 from math import sin, cos, tan, pi, radians
7 from bpy.types import Operator
8 from bpy.props import (
9 FloatProperty,
10 IntProperty,
11 BoolProperty,
12 StringProperty,
14 from bpy_extras import object_utils
16 # Create a new mesh (object) from verts/edges/faces.
17 # verts/edges/faces ... List of vertices/edges/faces for the
18 # new mesh (as used in from_pydata)
19 # name ... Name of the new mesh (& object)
21 def create_mesh(context, verts, edges, faces, name):
22 # Create new mesh
23 mesh = bpy.data.meshes.new(name)
25 # Make a mesh from a list of verts/edges/faces.
26 mesh.from_pydata(verts, edges, faces)
28 # Update mesh geometry after adding stuff.
29 mesh.update()
31 return mesh
33 # A very simple "bridge" tool.
35 def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False):
36 faces = []
38 if not vertIdx1 or not vertIdx2:
39 return None
41 if len(vertIdx1) < 2 and len(vertIdx2) < 2:
42 return None
44 fan = False
45 if (len(vertIdx1) != len(vertIdx2)):
46 if (len(vertIdx1) == 1 and len(vertIdx2) > 1):
47 fan = True
48 else:
49 return None
51 total = len(vertIdx2)
53 if closed:
54 # Bridge the start with the end.
55 if flipped:
56 face = [
57 vertIdx1[0],
58 vertIdx2[0],
59 vertIdx2[total - 1]]
60 if not fan:
61 face.append(vertIdx1[total - 1])
62 faces.append(face)
64 else:
65 face = [vertIdx2[0], vertIdx1[0]]
66 if not fan:
67 face.append(vertIdx1[total - 1])
68 face.append(vertIdx2[total - 1])
69 faces.append(face)
71 # Bridge the rest of the faces.
72 for num in range(total - 1):
73 if flipped:
74 if fan:
75 face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]]
76 else:
77 face = [vertIdx2[num], vertIdx1[num],
78 vertIdx1[num + 1], vertIdx2[num + 1]]
79 faces.append(face)
80 else:
81 if fan:
82 face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]]
83 else:
84 face = [vertIdx1[num], vertIdx2[num],
85 vertIdx2[num + 1], vertIdx1[num + 1]]
86 faces.append(face)
88 return faces
91 # Create the vertices and polygons for a simple elbow (bent pipe)
92 def ElbowJointParameters():
93 ElbowJointParameters = [
94 "radius",
95 "div",
96 "angle",
97 "startLength",
98 "endLength",
100 return ElbowJointParameters
102 class AddElbowJoint(Operator, object_utils.AddObjectHelper):
103 bl_idname = "mesh.primitive_elbow_joint_add"
104 bl_label = "Add Pipe Elbow"
105 bl_description = "Construct an elbow pipe mesh"
106 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
108 ElbowJoint : BoolProperty(name = "ElbowJoint",
109 default = True,
110 description = "ElbowJoint")
112 #### change properties
113 change : BoolProperty(name = "Change",
114 default = False,
115 description = "change ElbowJoint")
117 radius: FloatProperty(
118 name="Radius",
119 description="The radius of the pipe",
120 default=1.0,
121 min=0.01,
122 max=100.0,
123 unit="LENGTH"
125 div: IntProperty(
126 name="Divisions",
127 description="Number of vertices (divisions)",
128 default=32, min=3, max=256
130 angle: FloatProperty(
131 name="Angle",
132 description="The angle of the branching pipe (i.e. the 'arm' - "
133 "Measured from the center line of the main pipe",
134 default=radians(45.0),
135 min=radians(-179.9),
136 max=radians(179.9),
137 unit="ROTATION"
139 startLength: FloatProperty(
140 name="Length Start",
141 description="Length of the beginning of the pipe",
142 default=3.0,
143 min=0.01,
144 max=100.0,
145 unit="LENGTH"
147 endLength: FloatProperty(
148 name="End Length",
149 description="Length of the end of the pipe",
150 default=3.0,
151 min=0.01,
152 max=100.0,
153 unit="LENGTH"
156 def draw(self, context):
157 layout = self.layout
159 box = layout.box()
160 box.prop(self, 'radius')
161 box.prop(self, 'div')
162 box.prop(self, 'angle')
163 box.prop(self, 'startLength')
164 box.prop(self, 'endLength')
166 if self.change == False:
167 # generic transform props
168 box = layout.box()
169 box.prop(self, 'align', expand=True)
170 box.prop(self, 'location', expand=True)
171 box.prop(self, 'rotation', expand=True)
173 def execute(self, context):
174 # turn off 'Enter Edit Mode'
175 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
176 bpy.context.preferences.edit.use_enter_edit_mode = False
178 radius = self.radius
179 div = self.div
181 angle = self.angle
183 startLength = self.startLength
184 endLength = self.endLength
186 verts = []
187 faces = []
189 loop1 = [] # The starting circle
190 loop2 = [] # The elbow circle
191 loop3 = [] # The end circle
193 # Create start circle
194 for vertIdx in range(div):
195 curVertAngle = vertIdx * (2.0 * pi / div)
196 locX = sin(curVertAngle)
197 locY = cos(curVertAngle)
198 locZ = -startLength
199 loop1.append(len(verts))
200 verts.append([locX * radius, locY * radius, locZ])
202 # Create deformed joint circle
203 for vertIdx in range(div):
204 curVertAngle = vertIdx * (2.0 * pi / div)
205 locX = sin(curVertAngle)
206 locY = cos(curVertAngle)
207 locZ = locX * tan(angle / 2.0)
208 loop2.append(len(verts))
209 verts.append([locX * radius, locY * radius, locZ * radius])
211 # Create end circle
212 baseEndLocX = -endLength * sin(angle)
213 baseEndLocZ = endLength * cos(angle)
214 for vertIdx in range(div):
215 curVertAngle = vertIdx * (2.0 * pi / div)
216 # Create circle
217 locX = sin(curVertAngle) * radius
218 locY = cos(curVertAngle) * radius
219 locZ = 0.0
221 # Rotate circle
222 locZ = locX * cos(pi / 2.0 - angle)
223 locX = locX * sin(pi / 2.0 - angle)
225 loop3.append(len(verts))
226 # Translate and add circle vertices to the list.
227 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
229 # Create faces
230 faces.extend(createFaces(loop1, loop2, closed=True))
231 faces.extend(createFaces(loop2, loop3, closed=True))
233 if bpy.context.mode == "OBJECT":
234 if (context.selected_objects != []) and context.active_object and \
235 (context.active_object.data is not None) and ('ElbowJoint' in context.active_object.data.keys()) and \
236 (self.change == True):
237 obj = context.active_object
238 oldmesh = obj.data
239 oldmeshname = obj.data.name
240 mesh = create_mesh(context, verts, [], faces, "Elbow Joint")
241 obj.data = mesh
242 for material in oldmesh.materials:
243 obj.data.materials.append(material)
244 bpy.data.meshes.remove(oldmesh)
245 obj.data.name = oldmeshname
246 else:
247 mesh = create_mesh(context, verts, [], faces, "Elbow Joint")
248 obj = object_utils.object_data_add(context, mesh, operator=self)
250 mesh.update()
252 obj.data["ElbowJoint"] = True
253 obj.data["change"] = False
254 for prm in ElbowJointParameters():
255 obj.data[prm] = getattr(self, prm)
257 if bpy.context.mode == "EDIT_MESH":
258 active_object = context.active_object
259 name_active_object = active_object.name
260 bpy.ops.object.mode_set(mode='OBJECT')
261 mesh = create_mesh(context, verts, [], faces, "TMP")
262 obj = object_utils.object_data_add(context, mesh, operator=self)
263 obj.select_set(True)
264 active_object.select_set(True)
265 bpy.context.view_layer.objects.active = active_object
266 bpy.ops.object.join()
267 context.active_object.name = name_active_object
268 bpy.ops.object.mode_set(mode='EDIT')
270 if use_enter_edit_mode:
271 bpy.ops.object.mode_set(mode = 'EDIT')
273 # restore pre operator state
274 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
276 return {'FINISHED'}
279 # Create the vertices and polygons for a simple tee (T) joint
280 # The base arm of the T can be positioned in an angle if needed though
281 def TeeJointParameters():
282 TeeJointParameters = [
283 "radius",
284 "div",
285 "angle",
286 "startLength",
287 "endLength",
288 "branchLength",
290 return TeeJointParameters
292 class AddTeeJoint(Operator, object_utils.AddObjectHelper):
293 bl_idname = "mesh.primitive_tee_joint_add"
294 bl_label = "Add Pipe Tee-Joint"
295 bl_description = "Construct a tee-joint pipe mesh"
296 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
298 TeeJoint : BoolProperty(name = "TeeJoint",
299 default = True,
300 description = "TeeJoint")
302 #### change properties
303 change : BoolProperty(name = "Change",
304 default = False,
305 description = "change TeeJoint")
307 radius: FloatProperty(
308 name="Radius",
309 description="The radius of the pipe",
310 default=1.0,
311 min=0.01,
312 max=100.0,
313 unit="LENGTH"
315 div: IntProperty(
316 name="Divisions",
317 description="Number of vertices (divisions)",
318 default=32,
319 min=4,
320 max=256
322 angle: FloatProperty(
323 name="Angle",
324 description="The angle of the branching pipe (i.e. the 'arm' - "
325 "Measured from the center line of the main pipe",
326 default=radians(90.0),
327 min=radians(0.1),
328 max=radians(179.9),
329 unit="ROTATION"
331 startLength: FloatProperty(
332 name="Length Start",
333 description="Length of the beginning of the"
334 " main pipe (the straight one)",
335 default=3.0,
336 min=0.01,
337 max=100.0,
338 unit="LENGTH"
340 endLength: FloatProperty(
341 name="End Length",
342 description="Length of the end of the"
343 " main pipe (the straight one)",
344 default=3.0,
345 min=0.01,
346 max=100.0,
347 unit="LENGTH"
349 branchLength: FloatProperty(
350 name="Arm Length",
351 description="Length of the arm pipe (the bent one)",
352 default=3.0,
353 min=0.01,
354 max=100.0,
355 unit="LENGTH"
358 def draw(self, context):
359 layout = self.layout
361 box = layout.box()
362 box.prop(self, 'radius')
363 box.prop(self, 'div')
364 box.prop(self, 'angle')
365 box.prop(self, 'startLength')
366 box.prop(self, 'endLength')
367 box.prop(self, 'branchLength')
369 if self.change == False:
370 # generic transform props
371 box = layout.box()
372 box.prop(self, 'align', expand=True)
373 box.prop(self, 'location', expand=True)
374 box.prop(self, 'rotation', expand=True)
376 def execute(self, context):
377 # turn off 'Enter Edit Mode'
378 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
379 bpy.context.preferences.edit.use_enter_edit_mode = False
381 radius = self.radius
382 div = self.div
384 angle = self.angle
386 startLength = self.startLength
387 endLength = self.endLength
388 branchLength = self.branchLength
390 if (div % 2):
391 # Odd vertice number not supported (yet)
392 self.report({'INFO'}, "Odd vertices number is not yet supported")
393 return {'CANCELLED'}
395 verts = []
396 faces = []
398 # List of vert indices of each cross section
399 loopMainStart = [] # Vert indices for the beginning of the main pipe
400 loopJoint1 = [] # Vert indices for joint that is used to connect the joint & loopMainStart
401 loopJoint2 = [] # Vert indices for joint that is used to connect the joint & loopArm
402 loopJoint3 = [] # Vert index for joint that is used to connect the joint & loopMainEnd
403 loopArm = [] # Vert indices for the end of the arm
404 loopMainEnd = [] # Vert indices for the end of the main pipe.
406 # Create start circle (main pipe)
407 for vertIdx in range(div):
408 curVertAngle = vertIdx * (2.0 * pi / div)
409 locX = sin(curVertAngle)
410 locY = cos(curVertAngle)
411 locZ = -startLength
412 loopMainStart.append(len(verts))
413 verts.append([locX * radius, locY * radius, locZ])
415 # Create deformed joint circle
416 vertTemp1 = None
417 vertTemp2 = None
418 for vertIdx in range(div):
419 curVertAngle = vertIdx * (2.0 * pi / div)
420 locX = sin(curVertAngle)
421 locY = cos(curVertAngle)
423 if vertIdx == 0:
424 vertTemp1 = len(verts)
425 if vertIdx == div / 2:
426 # @todo: This will possibly break if we
427 # ever support odd divisions.
428 vertTemp2 = len(verts)
430 loopJoint1.append(len(verts))
431 if (vertIdx < div / 2):
432 # Straight side of main pipe.
433 locZ = 0
434 loopJoint3.append(len(verts))
435 else:
436 # Branching side
437 locZ = locX * tan(angle / 2.0)
438 loopJoint2.append(len(verts))
440 verts.append([locX * radius, locY * radius, locZ * radius])
442 # Create 2. deformed joint (half-)circle
443 loopTemp = []
444 for vertIdx in range(div):
445 if (vertIdx > div / 2):
446 curVertAngle = vertIdx * (2.0 * pi / div)
447 locX = sin(curVertAngle)
448 locY = -cos(curVertAngle)
449 locZ = -(radius * locX * tan((pi - angle) / 2.0))
450 loopTemp.append(len(verts))
451 verts.append([locX * radius, locY * radius, locZ])
453 loopTemp2 = loopTemp[:]
455 # Finalise 2. loop
456 loopTemp.reverse()
457 loopTemp.append(vertTemp1)
458 loopJoint2.reverse()
459 loopJoint2.extend(loopTemp)
460 loopJoint2.reverse()
462 # Finalise 3. loop
463 loopTemp2.append(vertTemp2)
464 loopTemp2.reverse()
465 loopJoint3.extend(loopTemp2)
467 # Create end circle (branching pipe)
468 baseEndLocX = -branchLength * sin(angle)
469 baseEndLocZ = branchLength * cos(angle)
470 for vertIdx in range(div):
471 curVertAngle = vertIdx * (2.0 * pi / div)
472 # Create circle
473 locX = sin(curVertAngle) * radius
474 locY = cos(curVertAngle) * radius
475 locZ = 0.0
477 # Rotate circle
478 locZ = locX * cos(pi / 2.0 - angle)
479 locX = locX * sin(pi / 2.0 - angle)
481 loopArm.append(len(verts))
483 # Add translated circle.
484 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
486 # Create end circle (main pipe)
487 for vertIdx in range(div):
488 curVertAngle = vertIdx * (2.0 * pi / div)
489 locX = sin(curVertAngle)
490 locY = cos(curVertAngle)
491 locZ = endLength
492 loopMainEnd.append(len(verts))
493 verts.append([locX * radius, locY * radius, locZ])
495 # Create faces
496 faces.extend(createFaces(loopMainStart, loopJoint1, closed=True))
497 faces.extend(createFaces(loopJoint2, loopArm, closed=True))
498 faces.extend(createFaces(loopJoint3, loopMainEnd, closed=True))
500 if bpy.context.mode == "OBJECT":
501 if (context.selected_objects != []) and context.active_object and \
502 (context.active_object.data is not None) and ('TeeJoint' in context.active_object.data.keys()) and \
503 (self.change == True):
504 obj = context.active_object
505 oldmesh = obj.data
506 oldmeshname = obj.data.name
507 mesh = create_mesh(context, verts, [], faces, "Tee Joint")
508 obj.data = mesh
509 for material in oldmesh.materials:
510 obj.data.materials.append(material)
511 bpy.data.meshes.remove(oldmesh)
512 obj.data.name = oldmeshname
513 else:
514 mesh = create_mesh(context, verts, [], faces, "Tee Joint")
515 obj = object_utils.object_data_add(context, mesh, operator=self)
517 mesh.update()
519 obj.data["TeeJoint"] = True
520 obj.data["change"] = False
521 for prm in TeeJointParameters():
522 obj.data[prm] = getattr(self, prm)
524 if bpy.context.mode == "EDIT_MESH":
525 active_object = context.active_object
526 name_active_object = active_object.name
527 bpy.ops.object.mode_set(mode='OBJECT')
528 mesh = create_mesh(context, verts, [], faces, "TMP")
529 obj = object_utils.object_data_add(context, mesh, operator=self)
530 obj.select_set(True)
531 active_object.select_set(True)
532 bpy.context.view_layer.objects.active = active_object
533 bpy.ops.object.join()
534 context.active_object.name = name_active_object
535 bpy.ops.object.mode_set(mode='EDIT')
537 if use_enter_edit_mode:
538 bpy.ops.object.mode_set(mode = 'EDIT')
540 # restore pre operator state
541 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
543 return {'FINISHED'}
545 def WyeJointParameters():
546 WyeJointParameters = [
547 "radius",
548 "div",
549 "angle1",
550 "angle2",
551 "startLength",
552 "branch1Length",
553 "branch2Length",
555 return WyeJointParameters
557 class AddWyeJoint(Operator, object_utils.AddObjectHelper):
558 bl_idname = "mesh.primitive_wye_joint_add"
559 bl_label = "Add Pipe Wye-Joint"
560 bl_description = "Construct a wye-joint pipe mesh"
561 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
563 WyeJoint : BoolProperty(name = "WyeJoint",
564 default = True,
565 description = "WyeJoint")
567 #### change properties
568 change : BoolProperty(name = "Change",
569 default = False,
570 description = "change WyeJoint")
572 radius: FloatProperty(
573 name="Radius",
574 description="The radius of the pipe",
575 default=1.0,
576 min=0.01,
577 max=100.0,
578 unit="LENGTH"
580 div: IntProperty(
581 name="Divisions",
582 description="Number of vertices (divisions)",
583 default=32,
584 min=4,
585 max=256
587 angle1: FloatProperty(
588 name="Angle 1",
589 description="The angle of the 1. branching pipe "
590 "(measured from the center line of the main pipe)",
591 default=radians(45.0),
592 min=radians(-179.9),
593 max=radians(179.9),
594 unit="ROTATION"
596 angle2: FloatProperty(
597 name="Angle 2",
598 description="The angle of the 2. branching pipe "
599 "(measured from the center line of the main pipe) ",
600 default=radians(45.0),
601 min=radians(-179.9),
602 max=radians(179.9),
603 unit="ROTATION"
605 startLength: FloatProperty(
606 name="Length Start",
607 description="Length of the beginning of the"
608 " main pipe (the straight one)",
609 default=3.0,
610 min=0.01,
611 max=100.0,
612 unit="LENGTH"
614 branch1Length: FloatProperty(
615 name="Length Arm 1",
616 description="Length of the 1. arm",
617 default=3.0,
618 min=0.01,
619 max=100.0,
620 unit="LENGTH"
622 branch2Length: FloatProperty(
623 name="Length Arm 2",
624 description="Length of the 2. arm",
625 default=3.0,
626 min=0.01,
627 max=100.0,
628 unit="LENGTH"
631 def draw(self, context):
632 layout = self.layout
634 box = layout.box()
635 box.prop(self, 'radius')
636 box.prop(self, 'div')
637 box.prop(self, 'angle1')
638 box.prop(self, 'angle2')
639 box.prop(self, 'startLength')
640 box.prop(self, 'branch1Length')
641 box.prop(self, 'branch2Length')
643 if self.change == False:
644 # generic transform props
645 box = layout.box()
646 box.prop(self, 'align', expand=True)
647 box.prop(self, 'location', expand=True)
648 box.prop(self, 'rotation', expand=True)
650 def execute(self, context):
651 # turn off 'Enter Edit Mode'
652 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
653 bpy.context.preferences.edit.use_enter_edit_mode = False
655 radius = self.radius
656 div = self.div
658 angle1 = self.angle1
659 angle2 = self.angle2
661 startLength = self.startLength
662 branch1Length = self.branch1Length
663 branch2Length = self.branch2Length
665 if (div % 2):
666 # Odd vertice number not supported (yet)
667 self.report({'INFO'}, "Odd vertices number is not yet supported")
668 return {'CANCELLED'}
670 verts = []
671 faces = []
673 # List of vert indices of each cross section
674 loopMainStart = [] # Vert indices for the beginning of the main pipe
675 loopJoint1 = [] # Vert index for joint that is used to connect the joint & loopMainStart
676 loopJoint2 = [] # Vert index for joint that is used to connect the joint & loopArm1
677 loopJoint3 = [] # Vert index for joint that is used to connect the joint & loopArm2
678 loopArm1 = [] # Vert idxs for end of the 1. arm
679 loopArm2 = [] # Vert idxs for end of the 2. arm
681 # Create start circle
682 for vertIdx in range(div):
683 curVertAngle = vertIdx * (2.0 * pi / div)
684 locX = sin(curVertAngle)
685 locY = cos(curVertAngle)
686 locZ = -startLength
687 loopMainStart.append(len(verts))
688 verts.append([locX * radius, locY * radius, locZ])
690 # Create deformed joint circle
691 vertTemp1 = None
692 vertTemp2 = None
693 for vertIdx in range(div):
694 curVertAngle = vertIdx * (2.0 * pi / div)
695 locX = sin(curVertAngle)
696 locY = cos(curVertAngle)
698 if vertIdx == 0:
699 vertTemp2 = len(verts)
700 if vertIdx == div / 2:
701 # @todo: This will possibly break if we
702 # ever support odd divisions.
703 vertTemp1 = len(verts)
705 loopJoint1.append(len(verts))
706 if (vertIdx > div / 2):
707 locZ = locX * tan(angle1 / 2.0)
708 loopJoint2.append(len(verts))
709 else:
710 locZ = locX * tan(-angle2 / 2.0)
711 loopJoint3.append(len(verts))
713 verts.append([locX * radius, locY * radius, locZ * radius])
715 # Create 2. deformed joint (half-)circle
716 loopTemp = []
717 angleJoint = (angle2 - angle1) / 2.0
718 for vertIdx in range(div):
719 if (vertIdx > div / 2):
720 curVertAngle = vertIdx * (2.0 * pi / div)
722 locX = (-sin(curVertAngle) * sin(angleJoint) / sin(angle2 - angleJoint))
723 locY = -cos(curVertAngle)
724 locZ = (-(sin(curVertAngle) * cos(angleJoint) / sin(angle2 - angleJoint)))
726 loopTemp.append(len(verts))
727 verts.append([locX * radius, locY * radius, locZ * radius])
729 loopTemp2 = loopTemp[:]
731 # Finalise 2. loop
732 loopTemp.append(vertTemp1)
733 loopTemp.reverse()
734 loopTemp.append(vertTemp2)
735 loopJoint2.reverse()
736 loopJoint2.extend(loopTemp)
737 loopJoint2.reverse()
739 # Finalise 3. loop
740 loopTemp2.reverse()
741 loopJoint3.extend(loopTemp2)
743 # Create end circle (1. branching pipe)
744 baseEndLocX = -branch1Length * sin(angle1)
745 baseEndLocZ = branch1Length * cos(angle1)
746 for vertIdx in range(div):
747 curVertAngle = vertIdx * (2.0 * pi / div)
748 # Create circle
749 locX = sin(curVertAngle) * radius
750 locY = cos(curVertAngle) * radius
751 locZ = 0.0
753 # Rotate circle
754 locZ = locX * cos(pi / 2.0 - angle1)
755 locX = locX * sin(pi / 2.0 - angle1)
757 loopArm1.append(len(verts))
758 # Add translated circle.
759 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
761 # Create end circle (2. branching pipe)
762 baseEndLocX = branch2Length * sin(angle2)
763 baseEndLocZ = branch2Length * cos(angle2)
764 for vertIdx in range(div):
765 curVertAngle = vertIdx * (2.0 * pi / div)
766 # Create circle
767 locX = sin(curVertAngle) * radius
768 locY = cos(curVertAngle) * radius
769 locZ = 0.0
771 # Rotate circle
772 locZ = locX * cos(pi / 2.0 + angle2)
773 locX = locX * sin(pi / 2.0 + angle2)
775 loopArm2.append(len(verts))
776 # Add translated circle
777 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
779 # Create faces
780 faces.extend(createFaces(loopMainStart, loopJoint1, closed=True))
781 faces.extend(createFaces(loopJoint2, loopArm1, closed=True))
782 faces.extend(createFaces(loopJoint3, loopArm2, closed=True))
784 if bpy.context.mode == "OBJECT":
785 if (context.selected_objects != []) and context.active_object and \
786 (context.active_object.data is not None) and ('WyeJoint' in context.active_object.data.keys()) and \
787 (self.change == True):
788 obj = context.active_object
789 oldmesh = obj.data
790 oldmeshname = obj.data.name
791 mesh = create_mesh(context, verts, [], faces, "Wye Joint")
792 obj.data = mesh
793 for material in oldmesh.materials:
794 obj.data.materials.append(material)
795 bpy.data.meshes.remove(oldmesh)
796 obj.data.name = oldmeshname
797 else:
798 mesh = create_mesh(context, verts, [], faces, "Wye Joint")
799 obj = object_utils.object_data_add(context, mesh, operator=self)
801 mesh.update()
803 obj.data["WyeJoint"] = True
804 obj.data["change"] = False
805 for prm in WyeJointParameters():
806 obj.data[prm] = getattr(self, prm)
808 if bpy.context.mode == "EDIT_MESH":
809 active_object = context.active_object
810 name_active_object = active_object.name
811 bpy.ops.object.mode_set(mode='OBJECT')
812 mesh = create_mesh(context, verts, [], faces, "TMP")
813 obj = object_utils.object_data_add(context, mesh, operator=self)
814 obj.select_set(True)
815 active_object.select_set(True)
816 bpy.context.view_layer.objects.active = active_object
817 bpy.ops.object.join()
818 context.active_object.name = name_active_object
819 bpy.ops.object.mode_set(mode='EDIT')
821 if use_enter_edit_mode:
822 bpy.ops.object.mode_set(mode = 'EDIT')
824 # restore pre operator state
825 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
827 return {'FINISHED'}
830 # Create the vertices and polygons for a cross (+ or X) pipe joint
831 def CrossJointParameters():
832 CrossJointParameters = [
833 "radius",
834 "div",
835 "angle1",
836 "angle2",
837 "angle3",
838 "startLength",
839 "branch1Length",
840 "branch2Length",
841 "branch3Length",
843 return CrossJointParameters
845 class AddCrossJoint(Operator, object_utils.AddObjectHelper):
846 bl_idname = "mesh.primitive_cross_joint_add"
847 bl_label = "Add Pipe Cross-Joint"
848 bl_description = "Construct a cross-joint pipe mesh"
849 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
851 CrossJoint : BoolProperty(name = "CrossJoint",
852 default = True,
853 description = "CrossJoint")
855 #### change properties
856 change : BoolProperty(name = "Change",
857 default = False,
858 description = "change CrossJoint")
860 radius: FloatProperty(
861 name="Radius",
862 description="The radius of the pipe",
863 default=1.0,
864 min=0.01,
865 max=100.0,
866 unit="LENGTH"
868 div: IntProperty(
869 name="Divisions",
870 description="Number of vertices (divisions)",
871 default=32,
872 min=4,
873 max=256
875 angle1: FloatProperty(
876 name="Angle 1",
877 description="The angle of the 1. arm (from the main axis)",
878 default=radians(90.0),
879 min=radians(-179.9),
880 max=radians(179.9),
881 unit="ROTATION"
883 angle2: FloatProperty(name="Angle 2",
884 description="The angle of the 2. arm (from the main axis)",
885 default=radians(90.0),
886 min=radians(-179.9),
887 max=radians(179.9),
888 unit="ROTATION"
890 angle3: FloatProperty(name="Angle 3 (center)",
891 description="The angle of the center arm (from the main axis)",
892 default=radians(0.0),
893 min=radians(-179.9),
894 max=radians(179.9),
895 unit="ROTATION"
897 startLength: FloatProperty(
898 name="Length Start",
899 description="Length of the beginning of the "
900 "main pipe (the straight one)",
901 default=3.0,
902 min=0.01,
903 max=100.0,
904 unit="LENGTH"
906 branch1Length: FloatProperty(name="Length Arm 1",
907 description="Length of the 1. arm",
908 default=3.0,
909 min=0.01,
910 max=100.0,
911 unit="LENGTH"
913 branch2Length: FloatProperty(
914 name="Length Arm 2",
915 description="Length of the 2. arm",
916 default=3.0,
917 min=0.01,
918 max=100.0,
919 unit="LENGTH"
921 branch3Length: FloatProperty(
922 name="Length Arm 3 (center)",
923 description="Length of the center arm",
924 default=3.0,
925 min=0.01,
926 max=100.0,
927 unit="LENGTH"
930 def draw(self, context):
931 layout = self.layout
933 box = layout.box()
934 box.prop(self, 'radius')
935 box.prop(self, 'div')
936 box.prop(self, 'angle1')
937 box.prop(self, 'angle2')
938 box.prop(self, 'angle3')
939 box.prop(self, 'startLength')
940 box.prop(self, 'branch1Length')
941 box.prop(self, 'branch2Length')
942 box.prop(self, 'branch3Length')
944 if self.change == False:
945 # generic transform props
946 box = layout.box()
947 box.prop(self, 'align', expand=True)
948 box.prop(self, 'location', expand=True)
949 box.prop(self, 'rotation', expand=True)
951 def execute(self, context):
952 # turn off 'Enter Edit Mode'
953 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
954 bpy.context.preferences.edit.use_enter_edit_mode = False
956 radius = self.radius
957 div = self.div
959 angle1 = self.angle1
960 angle2 = self.angle2
961 angle3 = self.angle3
963 startLength = self.startLength
964 branch1Length = self.branch1Length
965 branch2Length = self.branch2Length
966 branch3Length = self.branch3Length
967 if (div % 2):
968 # Odd vertice number not supported (yet)
969 self.report({'INFO'}, "Odd vertices number is not yet supported")
970 return {'CANCELLED'}
972 verts = []
973 faces = []
975 # List of vert indices of each cross section
976 loopMainStart = [] # Vert indices for the beginning of the main pipe
977 loopJoint1 = [] # Vert index for joint that is used to connect the joint & loopMainStart
978 loopJoint2 = [] # Vert index for joint that is used to connect the joint & loopArm1
979 loopJoint3 = [] # Vert index for joint that is used to connect the joint & loopArm2
980 loopJoint4 = [] # Vert index for joint that is used to connect the joint & loopArm3
981 loopArm1 = [] # Vert idxs for the end of the 1. arm
982 loopArm2 = [] # Vert idxs for the end of the 2. arm
983 loopArm3 = [] # Vert idxs for the center arm end
985 # Create start circle
986 for vertIdx in range(div):
987 curVertAngle = vertIdx * (2.0 * pi / div)
988 locX = sin(curVertAngle)
989 locY = cos(curVertAngle)
990 locZ = -startLength
991 loopMainStart.append(len(verts))
992 verts.append([locX * radius, locY * radius, locZ])
994 # Create 1. deformed joint circle
995 vertTemp1 = None
996 vertTemp2 = None
997 for vertIdx in range(div):
998 curVertAngle = vertIdx * (2.0 * pi / div)
999 locX = sin(curVertAngle)
1000 locY = cos(curVertAngle)
1002 if vertIdx == 0:
1003 vertTemp2 = len(verts)
1004 if vertIdx == div / 2:
1005 # @todo: This will possibly break if we
1006 # ever support odd divisions.
1007 vertTemp1 = len(verts)
1009 loopJoint1.append(len(verts))
1010 if (vertIdx > div / 2):
1011 locZ = locX * tan(angle1 / 2.0)
1012 loopJoint2.append(len(verts))
1013 else:
1014 locZ = locX * tan(-angle2 / 2.0)
1015 loopJoint3.append(len(verts))
1017 verts.append([locX * radius, locY * radius, locZ * radius])
1019 # Create 2. deformed joint circle
1020 loopTempA = []
1021 loopTempB = []
1022 angleJoint1 = (angle1 - angle3) / 2.0
1023 angleJoint2 = (angle2 + angle3) / 2.0
1024 for vertIdx in range(div):
1025 curVertAngle = vertIdx * (2.0 * pi / div)
1027 # Skip pole vertices
1028 # @todo: This will possibly break if
1029 # we ever support odd divisions
1030 if not (vertIdx == 0) and not (vertIdx == div / 2):
1032 if (vertIdx > div / 2):
1033 angleJoint = angleJoint1
1034 angle = angle1
1035 Z = -1.0
1036 loopTempA.append(len(verts))
1038 else:
1039 angleJoint = angleJoint2
1040 angle = angle2
1041 Z = 1.0
1042 loopTempB.append(len(verts))
1044 locX = (sin(curVertAngle) * sin(angleJoint) / sin(angle - angleJoint))
1045 locY = -cos(curVertAngle)
1046 locZ = (Z * (sin(curVertAngle) * cos(angleJoint) / sin(angle - angleJoint)))
1048 verts.append([locX * radius, locY * radius, locZ * radius])
1050 loopTempA2 = loopTempA[:]
1051 loopTempB2 = loopTempB[:]
1052 loopTempB3 = loopTempB[:]
1054 # Finalise 2. loop
1055 loopTempA.append(vertTemp1)
1056 loopTempA.reverse()
1057 loopTempA.append(vertTemp2)
1058 loopJoint2.reverse()
1059 loopJoint2.extend(loopTempA)
1060 loopJoint2.reverse()
1062 # Finalise 3. loop
1063 loopJoint3.extend(loopTempB3)
1065 # Finalise 4. loop
1066 loopTempA2.append(vertTemp1)
1067 loopTempA2.reverse()
1068 loopTempB2.append(vertTemp2)
1069 loopJoint4.extend(reversed(loopTempB2))
1070 loopJoint4.extend(loopTempA2)
1072 # Create end circle (1. branching pipe)
1073 baseEndLocX = -branch1Length * sin(angle1)
1074 baseEndLocZ = branch1Length * cos(angle1)
1075 for vertIdx in range(div):
1076 curVertAngle = vertIdx * (2.0 * pi / div)
1077 # Create circle
1078 locX = sin(curVertAngle) * radius
1079 locY = cos(curVertAngle) * radius
1080 locZ = 0.0
1082 # Rotate circle
1083 locZ = locX * cos(pi / 2.0 - angle1)
1084 locX = locX * sin(pi / 2.0 - angle1)
1086 loopArm1.append(len(verts))
1087 # Add translated circle.
1088 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1090 # Create end circle (2. branching pipe)
1091 baseEndLocX = branch2Length * sin(angle2)
1092 baseEndLocZ = branch2Length * cos(angle2)
1093 for vertIdx in range(div):
1094 curVertAngle = vertIdx * (2.0 * pi / div)
1095 # Create circle
1096 locX = sin(curVertAngle) * radius
1097 locY = cos(curVertAngle) * radius
1098 locZ = 0.0
1100 # Rotate circle
1101 locZ = locX * cos(pi / 2.0 + angle2)
1102 locX = locX * sin(pi / 2.0 + angle2)
1104 loopArm2.append(len(verts))
1105 # Add translated circle
1106 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1108 # Create end circle (center pipe)
1109 baseEndLocX = branch3Length * sin(angle3)
1110 baseEndLocZ = branch3Length * cos(angle3)
1111 for vertIdx in range(div):
1112 curVertAngle = vertIdx * (2.0 * pi / div)
1113 # Create circle
1114 locX = sin(curVertAngle) * radius
1115 locY = cos(curVertAngle) * radius
1116 locZ = 0.0
1118 # Rotate circle
1119 locZ = locX * cos(pi / 2.0 + angle3)
1120 locX = locX * sin(pi / 2.0 + angle3)
1122 loopArm3.append(len(verts))
1123 # Add translated circle
1124 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1126 # Create faces
1127 faces.extend(createFaces(loopMainStart, loopJoint1, closed=True))
1128 faces.extend(createFaces(loopJoint2, loopArm1, closed=True))
1129 faces.extend(createFaces(loopJoint3, loopArm2, closed=True))
1130 faces.extend(createFaces(loopJoint4, loopArm3, closed=True))
1132 if bpy.context.mode == "OBJECT":
1133 if (context.selected_objects != []) and context.active_object and \
1134 (context.active_object.data is not None) and ('CrossJoint' in context.active_object.data.keys()) and \
1135 (self.change == True):
1136 obj = context.active_object
1137 oldmesh = obj.data
1138 oldmeshname = obj.data.name
1139 mesh = create_mesh(context, verts, [], faces, "Cross Joint")
1140 obj.data = mesh
1141 for material in oldmesh.materials:
1142 obj.data.materials.append(material)
1143 bpy.data.meshes.remove(oldmesh)
1144 obj.data.name = oldmeshname
1145 else:
1146 mesh = create_mesh(context, verts, [], faces, "Cross Joint")
1147 obj = object_utils.object_data_add(context, mesh, operator=self)
1149 mesh.update()
1151 obj.data["CrossJoint"] = True
1152 obj.data["change"] = False
1153 for prm in CrossJointParameters():
1154 obj.data[prm] = getattr(self, prm)
1156 if bpy.context.mode == "EDIT_MESH":
1157 active_object = context.active_object
1158 name_active_object = active_object.name
1159 bpy.ops.object.mode_set(mode='OBJECT')
1160 mesh = create_mesh(context, verts, [], faces, "TMP")
1161 obj = object_utils.object_data_add(context, mesh, operator=self)
1162 obj.select_set(True)
1163 active_object.select_set(True)
1164 bpy.context.view_layer.objects.active = active_object
1165 bpy.ops.object.join()
1166 context.active_object.name = name_active_object
1167 bpy.ops.object.mode_set(mode='EDIT')
1169 if use_enter_edit_mode:
1170 bpy.ops.object.mode_set(mode = 'EDIT')
1172 # restore pre operator state
1173 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
1175 return {'FINISHED'}
1178 # Create the vertices and polygons for a regular n-joint
1179 def NJointParameters():
1180 NJointParameters = [
1181 "radius",
1182 "div",
1183 "number",
1184 "length",
1186 return NJointParameters
1188 class AddNJoint(Operator, object_utils.AddObjectHelper):
1189 bl_idname = "mesh.primitive_n_joint_add"
1190 bl_label = "Add Pipe N-Joint"
1191 bl_description = "Construct a n-joint pipe mesh"
1192 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
1194 NJoint : BoolProperty(name = "NJoint",
1195 default = True,
1196 description = "NJoint")
1198 #### change properties
1199 change : BoolProperty(name = "Change",
1200 default = False,
1201 description = "change NJoint")
1203 radius: FloatProperty(
1204 name="Radius",
1205 description="The radius of the pipe",
1206 default=1.0,
1207 min=0.01,
1208 max=100.0,
1209 unit="LENGTH"
1211 div: IntProperty(
1212 name="Divisions",
1213 description="Number of vertices (divisions)",
1214 default=32,
1215 min=4,
1216 max=256
1218 number: IntProperty(
1219 name="Arms / Joints",
1220 description="Number of joints / arms",
1221 default=5,
1222 min=2,
1223 max=99999
1225 length: FloatProperty(
1226 name="Length",
1227 description="Length of each joint / arm",
1228 default=3.0,
1229 min=0.01,
1230 max=100.0,
1231 unit="LENGTH"
1234 def draw(self, context):
1235 layout = self.layout
1237 box = layout.box()
1238 box.prop(self, 'radius')
1239 box.prop(self, 'div')
1240 box.prop(self, 'number')
1241 box.prop(self, 'length')
1243 if self.change == False:
1244 # generic transform props
1245 box = layout.box()
1246 box.prop(self, 'align', expand=True)
1247 box.prop(self, 'location', expand=True)
1248 box.prop(self, 'rotation', expand=True)
1250 def execute(self, context):
1251 # turn off 'Enter Edit Mode'
1252 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
1253 bpy.context.preferences.edit.use_enter_edit_mode = False
1255 radius = self.radius
1256 div = self.div
1257 number = self.number
1258 length = self.length
1260 if (div % 2):
1261 # Odd vertice number not supported (yet)
1262 self.report({'INFO'}, "Odd vertices number is not yet supported")
1263 return {'CANCELLED'}
1265 if (number < 2):
1266 return {'CANCELLED'}
1268 verts = []
1269 faces = []
1271 loopsEndCircles = []
1272 loopsJointsTemp = []
1273 loopsJoints = []
1275 vertTemp1 = None
1276 vertTemp2 = None
1278 angleDiv = (2.0 * pi / number)
1280 # Create vertices for the end circles
1281 for num in range(number):
1282 circle = []
1283 # Create start circle
1284 angle = num * angleDiv
1286 baseEndLocX = length * sin(angle)
1287 baseEndLocZ = length * cos(angle)
1288 for vertIdx in range(div):
1289 curVertAngle = vertIdx * (2.0 * pi / div)
1290 # Create circle
1291 locX = sin(curVertAngle) * radius
1292 locY = cos(curVertAngle) * radius
1293 locZ = 0.0
1295 # Rotate circle
1296 locZ = locX * cos(pi / 2.0 + angle)
1297 locX = locX * sin(pi / 2.0 + angle)
1299 circle.append(len(verts))
1300 # Add translated circle
1301 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1303 loopsEndCircles.append(circle)
1305 # Create vertices for the joint circles
1306 loopJoint = []
1307 for vertIdx in range(div):
1308 curVertAngle = vertIdx * (2.0 * pi / div)
1309 locX = sin(curVertAngle)
1310 locY = cos(curVertAngle)
1312 skipVert = False
1313 # Store pole vertices
1314 if vertIdx == 0:
1315 if (num == 0):
1316 vertTemp2 = len(verts)
1317 else:
1318 skipVert = True
1319 elif vertIdx == div / 2:
1320 # @todo: This will possibly break if we
1321 # ever support odd divisions
1322 if (num == 0):
1323 vertTemp1 = len(verts)
1324 else:
1325 skipVert = True
1327 if not skipVert:
1328 if (vertIdx > div / 2):
1329 locZ = -locX * tan((pi - angleDiv) / 2.0)
1330 loopJoint.append(len(verts))
1332 # Rotate the vert
1333 cosAng = cos(-angle)
1334 sinAng = sin(-angle)
1335 LocXnew = locX * cosAng - locZ * sinAng
1336 LocZnew = locZ * cosAng + locX * sinAng
1337 locZ = LocZnew
1338 locX = LocXnew
1340 verts.append([
1341 locX * radius,
1342 locY * radius,
1343 locZ * radius])
1344 else:
1345 # These two vertices will only be
1346 # added the very first time.
1347 if vertIdx == 0 or vertIdx == div / 2:
1348 verts.append([locX * radius, locY * radius, locZ])
1350 loopsJointsTemp.append(loopJoint)
1352 # Create complete loops (loopsJoints) out of the
1353 # double number of half loops in loopsJointsTemp
1354 for halfLoopIdx in range(len(loopsJointsTemp)):
1355 if (halfLoopIdx == len(loopsJointsTemp) - 1):
1356 idx1 = halfLoopIdx
1357 idx2 = 0
1358 else:
1359 idx1 = halfLoopIdx
1360 idx2 = halfLoopIdx + 1
1362 loopJoint = []
1363 loopJoint.append(vertTemp2)
1364 loopJoint.extend(reversed(loopsJointsTemp[idx2]))
1365 loopJoint.append(vertTemp1)
1366 loopJoint.extend(loopsJointsTemp[idx1])
1368 loopsJoints.append(loopJoint)
1370 # Create faces from the two
1371 # loop arrays (loopsJoints -> loopsEndCircles)
1372 for loopIdx in range(len(loopsEndCircles)):
1373 faces.extend(
1374 createFaces(loopsJoints[loopIdx],
1375 loopsEndCircles[loopIdx], closed=True))
1377 if bpy.context.mode == "OBJECT":
1378 if (context.selected_objects != []) and context.active_object and \
1379 (context.active_object.data is not None) and ('NJoint' in context.active_object.data.keys()) and \
1380 (self.change == True):
1381 obj = context.active_object
1382 oldmesh = obj.data
1383 oldmeshname = obj.data.name
1384 mesh = create_mesh(context, verts, [], faces, "N Joint")
1385 obj.data = mesh
1386 for material in oldmesh.materials:
1387 obj.data.materials.append(material)
1388 bpy.data.meshes.remove(oldmesh)
1389 obj.data.name = oldmeshname
1390 else:
1391 mesh = create_mesh(context, verts, [], faces, "N Joint")
1392 obj = object_utils.object_data_add(context, mesh, operator=self)
1394 obj.data["NJoint"] = True
1395 obj.data["change"] = False
1396 for prm in NJointParameters():
1397 obj.data[prm] = getattr(self, prm)
1399 if bpy.context.mode == "EDIT_MESH":
1400 active_object = context.active_object
1401 name_active_object = active_object.name
1402 bpy.ops.object.mode_set(mode='OBJECT')
1403 mesh = create_mesh(context, verts, [], faces, "TMP")
1404 obj = object_utils.object_data_add(context, mesh, operator=self)
1405 obj.select_set(True)
1406 active_object.select_set(True)
1407 bpy.context.view_layer.objects.active = active_object
1408 bpy.ops.object.join()
1409 context.active_object.name = name_active_object
1410 bpy.ops.object.mode_set(mode='EDIT')
1412 if use_enter_edit_mode:
1413 bpy.ops.object.mode_set(mode = 'EDIT')
1415 # restore pre operator state
1416 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
1418 return {'FINISHED'}