Fix #105067: Node Wrangler: cannot preview multi-user material group
[blender-addons.git] / add_mesh_extra_objects / add_mesh_pipe_joint.py
blob8067906156b6f0ae140e44bcbb1e56266478b416
1 # SPDX-FileCopyrightText: 2010-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # Author: Buerbaum Martin (Pontiac)
7 import bpy, bmesh
8 from math import sin, cos, tan, pi, radians
9 from bpy.types import Operator
10 from bpy.props import (
11 FloatProperty,
12 IntProperty,
13 BoolProperty,
14 StringProperty,
16 from bpy_extras import object_utils
18 # Create a new mesh (object) from verts/edges/faces.
19 # verts/edges/faces ... List of vertices/edges/faces for the
20 # new mesh (as used in from_pydata)
21 # name ... Name of the new mesh (& object)
23 def create_mesh(context, verts, edges, faces, name):
24 # Create new mesh
25 mesh = bpy.data.meshes.new(name)
27 # Make a mesh from a list of verts/edges/faces.
28 mesh.from_pydata(verts, edges, faces)
30 # Update mesh geometry after adding stuff.
31 mesh.update()
33 return mesh
35 # A very simple "bridge" tool.
37 def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False):
38 faces = []
40 if not vertIdx1 or not vertIdx2:
41 return None
43 if len(vertIdx1) < 2 and len(vertIdx2) < 2:
44 return None
46 fan = False
47 if (len(vertIdx1) != len(vertIdx2)):
48 if (len(vertIdx1) == 1 and len(vertIdx2) > 1):
49 fan = True
50 else:
51 return None
53 total = len(vertIdx2)
55 if closed:
56 # Bridge the start with the end.
57 if flipped:
58 face = [
59 vertIdx1[0],
60 vertIdx2[0],
61 vertIdx2[total - 1]]
62 if not fan:
63 face.append(vertIdx1[total - 1])
64 faces.append(face)
66 else:
67 face = [vertIdx2[0], vertIdx1[0]]
68 if not fan:
69 face.append(vertIdx1[total - 1])
70 face.append(vertIdx2[total - 1])
71 faces.append(face)
73 # Bridge the rest of the faces.
74 for num in range(total - 1):
75 if flipped:
76 if fan:
77 face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]]
78 else:
79 face = [vertIdx2[num], vertIdx1[num],
80 vertIdx1[num + 1], vertIdx2[num + 1]]
81 faces.append(face)
82 else:
83 if fan:
84 face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]]
85 else:
86 face = [vertIdx1[num], vertIdx2[num],
87 vertIdx2[num + 1], vertIdx1[num + 1]]
88 faces.append(face)
90 return faces
93 # Create the vertices and polygons for a simple elbow (bent pipe)
94 def ElbowJointParameters():
95 ElbowJointParameters = [
96 "radius",
97 "div",
98 "angle",
99 "startLength",
100 "endLength",
102 return ElbowJointParameters
104 class AddElbowJoint(Operator, object_utils.AddObjectHelper):
105 bl_idname = "mesh.primitive_elbow_joint_add"
106 bl_label = "Add Pipe Elbow"
107 bl_description = "Construct an elbow pipe mesh"
108 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
110 ElbowJoint : BoolProperty(name = "ElbowJoint",
111 default = True,
112 description = "ElbowJoint")
114 #### change properties
115 change : BoolProperty(name = "Change",
116 default = False,
117 description = "change ElbowJoint")
119 radius: FloatProperty(
120 name="Radius",
121 description="The radius of the pipe",
122 default=1.0,
123 min=0.01,
124 max=100.0,
125 unit="LENGTH"
127 div: IntProperty(
128 name="Divisions",
129 description="Number of vertices (divisions)",
130 default=32, min=3, max=256
132 angle: FloatProperty(
133 name="Angle",
134 description="The angle of the branching pipe (i.e. the 'arm' - "
135 "Measured from the center line of the main pipe",
136 default=radians(45.0),
137 min=radians(-179.9),
138 max=radians(179.9),
139 unit="ROTATION"
141 startLength: FloatProperty(
142 name="Length Start",
143 description="Length of the beginning of the pipe",
144 default=3.0,
145 min=0.01,
146 max=100.0,
147 unit="LENGTH"
149 endLength: FloatProperty(
150 name="End Length",
151 description="Length of the end of the pipe",
152 default=3.0,
153 min=0.01,
154 max=100.0,
155 unit="LENGTH"
158 def draw(self, context):
159 layout = self.layout
161 box = layout.box()
162 box.prop(self, 'radius')
163 box.prop(self, 'div')
164 box.prop(self, 'angle')
165 box.prop(self, 'startLength')
166 box.prop(self, 'endLength')
168 if self.change == False:
169 # generic transform props
170 box = layout.box()
171 box.prop(self, 'align', expand=True)
172 box.prop(self, 'location', expand=True)
173 box.prop(self, 'rotation', expand=True)
175 def execute(self, context):
176 # turn off 'Enter Edit Mode'
177 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
178 bpy.context.preferences.edit.use_enter_edit_mode = False
180 radius = self.radius
181 div = self.div
183 angle = self.angle
185 startLength = self.startLength
186 endLength = self.endLength
188 verts = []
189 faces = []
191 loop1 = [] # The starting circle
192 loop2 = [] # The elbow circle
193 loop3 = [] # The end circle
195 # Create start circle
196 for vertIdx in range(div):
197 curVertAngle = vertIdx * (2.0 * pi / div)
198 locX = sin(curVertAngle)
199 locY = cos(curVertAngle)
200 locZ = -startLength
201 loop1.append(len(verts))
202 verts.append([locX * radius, locY * radius, locZ])
204 # Create deformed joint circle
205 for vertIdx in range(div):
206 curVertAngle = vertIdx * (2.0 * pi / div)
207 locX = sin(curVertAngle)
208 locY = cos(curVertAngle)
209 locZ = locX * tan(angle / 2.0)
210 loop2.append(len(verts))
211 verts.append([locX * radius, locY * radius, locZ * radius])
213 # Create end circle
214 baseEndLocX = -endLength * sin(angle)
215 baseEndLocZ = endLength * cos(angle)
216 for vertIdx in range(div):
217 curVertAngle = vertIdx * (2.0 * pi / div)
218 # Create circle
219 locX = sin(curVertAngle) * radius
220 locY = cos(curVertAngle) * radius
221 locZ = 0.0
223 # Rotate circle
224 locZ = locX * cos(pi / 2.0 - angle)
225 locX = locX * sin(pi / 2.0 - angle)
227 loop3.append(len(verts))
228 # Translate and add circle vertices to the list.
229 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
231 # Create faces
232 faces.extend(createFaces(loop1, loop2, closed=True))
233 faces.extend(createFaces(loop2, loop3, closed=True))
235 if bpy.context.mode == "OBJECT":
236 if (context.selected_objects != []) and context.active_object and \
237 (context.active_object.data is not None) and ('ElbowJoint' in context.active_object.data.keys()) and \
238 (self.change == True):
239 obj = context.active_object
240 oldmesh = obj.data
241 oldmeshname = obj.data.name
242 mesh = create_mesh(context, verts, [], faces, "Elbow Joint")
243 obj.data = mesh
244 for material in oldmesh.materials:
245 obj.data.materials.append(material)
246 bpy.data.meshes.remove(oldmesh)
247 obj.data.name = oldmeshname
248 else:
249 mesh = create_mesh(context, verts, [], faces, "Elbow Joint")
250 obj = object_utils.object_data_add(context, mesh, operator=self)
252 mesh.update()
254 obj.data["ElbowJoint"] = True
255 obj.data["change"] = False
256 for prm in ElbowJointParameters():
257 obj.data[prm] = getattr(self, prm)
259 if bpy.context.mode == "EDIT_MESH":
260 active_object = context.active_object
261 name_active_object = active_object.name
262 bpy.ops.object.mode_set(mode='OBJECT')
263 mesh = create_mesh(context, verts, [], faces, "TMP")
264 obj = object_utils.object_data_add(context, mesh, operator=self)
265 obj.select_set(True)
266 active_object.select_set(True)
267 bpy.context.view_layer.objects.active = active_object
268 bpy.ops.object.join()
269 context.active_object.name = name_active_object
270 bpy.ops.object.mode_set(mode='EDIT')
272 if use_enter_edit_mode:
273 bpy.ops.object.mode_set(mode = 'EDIT')
275 # restore pre operator state
276 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
278 return {'FINISHED'}
281 # Create the vertices and polygons for a simple tee (T) joint
282 # The base arm of the T can be positioned in an angle if needed though
283 def TeeJointParameters():
284 TeeJointParameters = [
285 "radius",
286 "div",
287 "angle",
288 "startLength",
289 "endLength",
290 "branchLength",
292 return TeeJointParameters
294 class AddTeeJoint(Operator, object_utils.AddObjectHelper):
295 bl_idname = "mesh.primitive_tee_joint_add"
296 bl_label = "Add Pipe Tee-Joint"
297 bl_description = "Construct a tee-joint pipe mesh"
298 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
300 TeeJoint : BoolProperty(name = "TeeJoint",
301 default = True,
302 description = "TeeJoint")
304 #### change properties
305 change : BoolProperty(name = "Change",
306 default = False,
307 description = "change TeeJoint")
309 radius: FloatProperty(
310 name="Radius",
311 description="The radius of the pipe",
312 default=1.0,
313 min=0.01,
314 max=100.0,
315 unit="LENGTH"
317 div: IntProperty(
318 name="Divisions",
319 description="Number of vertices (divisions)",
320 default=32,
321 min=4,
322 max=256
324 angle: FloatProperty(
325 name="Angle",
326 description="The angle of the branching pipe (i.e. the 'arm' - "
327 "Measured from the center line of the main pipe",
328 default=radians(90.0),
329 min=radians(0.1),
330 max=radians(179.9),
331 unit="ROTATION"
333 startLength: FloatProperty(
334 name="Length Start",
335 description="Length of the beginning of the"
336 " main pipe (the straight one)",
337 default=3.0,
338 min=0.01,
339 max=100.0,
340 unit="LENGTH"
342 endLength: FloatProperty(
343 name="End Length",
344 description="Length of the end of the"
345 " main pipe (the straight one)",
346 default=3.0,
347 min=0.01,
348 max=100.0,
349 unit="LENGTH"
351 branchLength: FloatProperty(
352 name="Arm Length",
353 description="Length of the arm pipe (the bent one)",
354 default=3.0,
355 min=0.01,
356 max=100.0,
357 unit="LENGTH"
360 def draw(self, context):
361 layout = self.layout
363 box = layout.box()
364 box.prop(self, 'radius')
365 box.prop(self, 'div')
366 box.prop(self, 'angle')
367 box.prop(self, 'startLength')
368 box.prop(self, 'endLength')
369 box.prop(self, 'branchLength')
371 if self.change == False:
372 # generic transform props
373 box = layout.box()
374 box.prop(self, 'align', expand=True)
375 box.prop(self, 'location', expand=True)
376 box.prop(self, 'rotation', expand=True)
378 def execute(self, context):
379 # turn off 'Enter Edit Mode'
380 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
381 bpy.context.preferences.edit.use_enter_edit_mode = False
383 radius = self.radius
384 div = self.div
386 angle = self.angle
388 startLength = self.startLength
389 endLength = self.endLength
390 branchLength = self.branchLength
392 if (div % 2):
393 # Odd vertice number not supported (yet)
394 self.report({'INFO'}, "Odd vertices number is not yet supported")
395 return {'CANCELLED'}
397 verts = []
398 faces = []
400 # List of vert indices of each cross section
401 loopMainStart = [] # Vert indices for the beginning of the main pipe
402 loopJoint1 = [] # Vert indices for joint that is used to connect the joint & loopMainStart
403 loopJoint2 = [] # Vert indices for joint that is used to connect the joint & loopArm
404 loopJoint3 = [] # Vert index for joint that is used to connect the joint & loopMainEnd
405 loopArm = [] # Vert indices for the end of the arm
406 loopMainEnd = [] # Vert indices for the end of the main pipe.
408 # Create start circle (main pipe)
409 for vertIdx in range(div):
410 curVertAngle = vertIdx * (2.0 * pi / div)
411 locX = sin(curVertAngle)
412 locY = cos(curVertAngle)
413 locZ = -startLength
414 loopMainStart.append(len(verts))
415 verts.append([locX * radius, locY * radius, locZ])
417 # Create deformed joint circle
418 vertTemp1 = None
419 vertTemp2 = None
420 for vertIdx in range(div):
421 curVertAngle = vertIdx * (2.0 * pi / div)
422 locX = sin(curVertAngle)
423 locY = cos(curVertAngle)
425 if vertIdx == 0:
426 vertTemp1 = len(verts)
427 if vertIdx == div / 2:
428 # @todo: This will possibly break if we
429 # ever support odd divisions.
430 vertTemp2 = len(verts)
432 loopJoint1.append(len(verts))
433 if (vertIdx < div / 2):
434 # Straight side of main pipe.
435 locZ = 0
436 loopJoint3.append(len(verts))
437 else:
438 # Branching side
439 locZ = locX * tan(angle / 2.0)
440 loopJoint2.append(len(verts))
442 verts.append([locX * radius, locY * radius, locZ * radius])
444 # Create 2. deformed joint (half-)circle
445 loopTemp = []
446 for vertIdx in range(div):
447 if (vertIdx > div / 2):
448 curVertAngle = vertIdx * (2.0 * pi / div)
449 locX = sin(curVertAngle)
450 locY = -cos(curVertAngle)
451 locZ = -(radius * locX * tan((pi - angle) / 2.0))
452 loopTemp.append(len(verts))
453 verts.append([locX * radius, locY * radius, locZ])
455 loopTemp2 = loopTemp[:]
457 # Finalise 2. loop
458 loopTemp.reverse()
459 loopTemp.append(vertTemp1)
460 loopJoint2.reverse()
461 loopJoint2.extend(loopTemp)
462 loopJoint2.reverse()
464 # Finalise 3. loop
465 loopTemp2.append(vertTemp2)
466 loopTemp2.reverse()
467 loopJoint3.extend(loopTemp2)
469 # Create end circle (branching pipe)
470 baseEndLocX = -branchLength * sin(angle)
471 baseEndLocZ = branchLength * cos(angle)
472 for vertIdx in range(div):
473 curVertAngle = vertIdx * (2.0 * pi / div)
474 # Create circle
475 locX = sin(curVertAngle) * radius
476 locY = cos(curVertAngle) * radius
477 locZ = 0.0
479 # Rotate circle
480 locZ = locX * cos(pi / 2.0 - angle)
481 locX = locX * sin(pi / 2.0 - angle)
483 loopArm.append(len(verts))
485 # Add translated circle.
486 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
488 # Create end circle (main pipe)
489 for vertIdx in range(div):
490 curVertAngle = vertIdx * (2.0 * pi / div)
491 locX = sin(curVertAngle)
492 locY = cos(curVertAngle)
493 locZ = endLength
494 loopMainEnd.append(len(verts))
495 verts.append([locX * radius, locY * radius, locZ])
497 # Create faces
498 faces.extend(createFaces(loopMainStart, loopJoint1, closed=True))
499 faces.extend(createFaces(loopJoint2, loopArm, closed=True))
500 faces.extend(createFaces(loopJoint3, loopMainEnd, closed=True))
502 if bpy.context.mode == "OBJECT":
503 if (context.selected_objects != []) and context.active_object and \
504 (context.active_object.data is not None) and ('TeeJoint' in context.active_object.data.keys()) and \
505 (self.change == True):
506 obj = context.active_object
507 oldmesh = obj.data
508 oldmeshname = obj.data.name
509 mesh = create_mesh(context, verts, [], faces, "Tee Joint")
510 obj.data = mesh
511 for material in oldmesh.materials:
512 obj.data.materials.append(material)
513 bpy.data.meshes.remove(oldmesh)
514 obj.data.name = oldmeshname
515 else:
516 mesh = create_mesh(context, verts, [], faces, "Tee Joint")
517 obj = object_utils.object_data_add(context, mesh, operator=self)
519 mesh.update()
521 obj.data["TeeJoint"] = True
522 obj.data["change"] = False
523 for prm in TeeJointParameters():
524 obj.data[prm] = getattr(self, prm)
526 if bpy.context.mode == "EDIT_MESH":
527 active_object = context.active_object
528 name_active_object = active_object.name
529 bpy.ops.object.mode_set(mode='OBJECT')
530 mesh = create_mesh(context, verts, [], faces, "TMP")
531 obj = object_utils.object_data_add(context, mesh, operator=self)
532 obj.select_set(True)
533 active_object.select_set(True)
534 bpy.context.view_layer.objects.active = active_object
535 bpy.ops.object.join()
536 context.active_object.name = name_active_object
537 bpy.ops.object.mode_set(mode='EDIT')
539 if use_enter_edit_mode:
540 bpy.ops.object.mode_set(mode = 'EDIT')
542 # restore pre operator state
543 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
545 return {'FINISHED'}
547 def WyeJointParameters():
548 WyeJointParameters = [
549 "radius",
550 "div",
551 "angle1",
552 "angle2",
553 "startLength",
554 "branch1Length",
555 "branch2Length",
557 return WyeJointParameters
559 class AddWyeJoint(Operator, object_utils.AddObjectHelper):
560 bl_idname = "mesh.primitive_wye_joint_add"
561 bl_label = "Add Pipe Wye-Joint"
562 bl_description = "Construct a wye-joint pipe mesh"
563 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
565 WyeJoint : BoolProperty(name = "WyeJoint",
566 default = True,
567 description = "WyeJoint")
569 #### change properties
570 change : BoolProperty(name = "Change",
571 default = False,
572 description = "change WyeJoint")
574 radius: FloatProperty(
575 name="Radius",
576 description="The radius of the pipe",
577 default=1.0,
578 min=0.01,
579 max=100.0,
580 unit="LENGTH"
582 div: IntProperty(
583 name="Divisions",
584 description="Number of vertices (divisions)",
585 default=32,
586 min=4,
587 max=256
589 angle1: FloatProperty(
590 name="Angle 1",
591 description="The angle of the 1. branching pipe "
592 "(measured from the center line of the main pipe)",
593 default=radians(45.0),
594 min=radians(-179.9),
595 max=radians(179.9),
596 unit="ROTATION"
598 angle2: FloatProperty(
599 name="Angle 2",
600 description="The angle of the 2. branching pipe "
601 "(measured from the center line of the main pipe) ",
602 default=radians(45.0),
603 min=radians(-179.9),
604 max=radians(179.9),
605 unit="ROTATION"
607 startLength: FloatProperty(
608 name="Length Start",
609 description="Length of the beginning of the"
610 " main pipe (the straight one)",
611 default=3.0,
612 min=0.01,
613 max=100.0,
614 unit="LENGTH"
616 branch1Length: FloatProperty(
617 name="Length Arm 1",
618 description="Length of the 1. arm",
619 default=3.0,
620 min=0.01,
621 max=100.0,
622 unit="LENGTH"
624 branch2Length: FloatProperty(
625 name="Length Arm 2",
626 description="Length of the 2. arm",
627 default=3.0,
628 min=0.01,
629 max=100.0,
630 unit="LENGTH"
633 def draw(self, context):
634 layout = self.layout
636 box = layout.box()
637 box.prop(self, 'radius')
638 box.prop(self, 'div')
639 box.prop(self, 'angle1')
640 box.prop(self, 'angle2')
641 box.prop(self, 'startLength')
642 box.prop(self, 'branch1Length')
643 box.prop(self, 'branch2Length')
645 if self.change == False:
646 # generic transform props
647 box = layout.box()
648 box.prop(self, 'align', expand=True)
649 box.prop(self, 'location', expand=True)
650 box.prop(self, 'rotation', expand=True)
652 def execute(self, context):
653 # turn off 'Enter Edit Mode'
654 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
655 bpy.context.preferences.edit.use_enter_edit_mode = False
657 radius = self.radius
658 div = self.div
660 angle1 = self.angle1
661 angle2 = self.angle2
663 startLength = self.startLength
664 branch1Length = self.branch1Length
665 branch2Length = self.branch2Length
667 if (div % 2):
668 # Odd vertice number not supported (yet)
669 self.report({'INFO'}, "Odd vertices number is not yet supported")
670 return {'CANCELLED'}
672 verts = []
673 faces = []
675 # List of vert indices of each cross section
676 loopMainStart = [] # Vert indices for the beginning of the main pipe
677 loopJoint1 = [] # Vert index for joint that is used to connect the joint & loopMainStart
678 loopJoint2 = [] # Vert index for joint that is used to connect the joint & loopArm1
679 loopJoint3 = [] # Vert index for joint that is used to connect the joint & loopArm2
680 loopArm1 = [] # Vert idxs for end of the 1. arm
681 loopArm2 = [] # Vert idxs for end of the 2. arm
683 # Create start circle
684 for vertIdx in range(div):
685 curVertAngle = vertIdx * (2.0 * pi / div)
686 locX = sin(curVertAngle)
687 locY = cos(curVertAngle)
688 locZ = -startLength
689 loopMainStart.append(len(verts))
690 verts.append([locX * radius, locY * radius, locZ])
692 # Create deformed joint circle
693 vertTemp1 = None
694 vertTemp2 = None
695 for vertIdx in range(div):
696 curVertAngle = vertIdx * (2.0 * pi / div)
697 locX = sin(curVertAngle)
698 locY = cos(curVertAngle)
700 if vertIdx == 0:
701 vertTemp2 = len(verts)
702 if vertIdx == div / 2:
703 # @todo: This will possibly break if we
704 # ever support odd divisions.
705 vertTemp1 = len(verts)
707 loopJoint1.append(len(verts))
708 if (vertIdx > div / 2):
709 locZ = locX * tan(angle1 / 2.0)
710 loopJoint2.append(len(verts))
711 else:
712 locZ = locX * tan(-angle2 / 2.0)
713 loopJoint3.append(len(verts))
715 verts.append([locX * radius, locY * radius, locZ * radius])
717 # Create 2. deformed joint (half-)circle
718 loopTemp = []
719 angleJoint = (angle2 - angle1) / 2.0
720 for vertIdx in range(div):
721 if (vertIdx > div / 2):
722 curVertAngle = vertIdx * (2.0 * pi / div)
724 locX = (-sin(curVertAngle) * sin(angleJoint) / sin(angle2 - angleJoint))
725 locY = -cos(curVertAngle)
726 locZ = (-(sin(curVertAngle) * cos(angleJoint) / sin(angle2 - angleJoint)))
728 loopTemp.append(len(verts))
729 verts.append([locX * radius, locY * radius, locZ * radius])
731 loopTemp2 = loopTemp[:]
733 # Finalise 2. loop
734 loopTemp.append(vertTemp1)
735 loopTemp.reverse()
736 loopTemp.append(vertTemp2)
737 loopJoint2.reverse()
738 loopJoint2.extend(loopTemp)
739 loopJoint2.reverse()
741 # Finalise 3. loop
742 loopTemp2.reverse()
743 loopJoint3.extend(loopTemp2)
745 # Create end circle (1. branching pipe)
746 baseEndLocX = -branch1Length * sin(angle1)
747 baseEndLocZ = branch1Length * cos(angle1)
748 for vertIdx in range(div):
749 curVertAngle = vertIdx * (2.0 * pi / div)
750 # Create circle
751 locX = sin(curVertAngle) * radius
752 locY = cos(curVertAngle) * radius
753 locZ = 0.0
755 # Rotate circle
756 locZ = locX * cos(pi / 2.0 - angle1)
757 locX = locX * sin(pi / 2.0 - angle1)
759 loopArm1.append(len(verts))
760 # Add translated circle.
761 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
763 # Create end circle (2. branching pipe)
764 baseEndLocX = branch2Length * sin(angle2)
765 baseEndLocZ = branch2Length * cos(angle2)
766 for vertIdx in range(div):
767 curVertAngle = vertIdx * (2.0 * pi / div)
768 # Create circle
769 locX = sin(curVertAngle) * radius
770 locY = cos(curVertAngle) * radius
771 locZ = 0.0
773 # Rotate circle
774 locZ = locX * cos(pi / 2.0 + angle2)
775 locX = locX * sin(pi / 2.0 + angle2)
777 loopArm2.append(len(verts))
778 # Add translated circle
779 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
781 # Create faces
782 faces.extend(createFaces(loopMainStart, loopJoint1, closed=True))
783 faces.extend(createFaces(loopJoint2, loopArm1, closed=True))
784 faces.extend(createFaces(loopJoint3, loopArm2, closed=True))
786 if bpy.context.mode == "OBJECT":
787 if (context.selected_objects != []) and context.active_object and \
788 (context.active_object.data is not None) and ('WyeJoint' in context.active_object.data.keys()) and \
789 (self.change == True):
790 obj = context.active_object
791 oldmesh = obj.data
792 oldmeshname = obj.data.name
793 mesh = create_mesh(context, verts, [], faces, "Wye Joint")
794 obj.data = mesh
795 for material in oldmesh.materials:
796 obj.data.materials.append(material)
797 bpy.data.meshes.remove(oldmesh)
798 obj.data.name = oldmeshname
799 else:
800 mesh = create_mesh(context, verts, [], faces, "Wye Joint")
801 obj = object_utils.object_data_add(context, mesh, operator=self)
803 mesh.update()
805 obj.data["WyeJoint"] = True
806 obj.data["change"] = False
807 for prm in WyeJointParameters():
808 obj.data[prm] = getattr(self, prm)
810 if bpy.context.mode == "EDIT_MESH":
811 active_object = context.active_object
812 name_active_object = active_object.name
813 bpy.ops.object.mode_set(mode='OBJECT')
814 mesh = create_mesh(context, verts, [], faces, "TMP")
815 obj = object_utils.object_data_add(context, mesh, operator=self)
816 obj.select_set(True)
817 active_object.select_set(True)
818 bpy.context.view_layer.objects.active = active_object
819 bpy.ops.object.join()
820 context.active_object.name = name_active_object
821 bpy.ops.object.mode_set(mode='EDIT')
823 if use_enter_edit_mode:
824 bpy.ops.object.mode_set(mode = 'EDIT')
826 # restore pre operator state
827 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
829 return {'FINISHED'}
832 # Create the vertices and polygons for a cross (+ or X) pipe joint
833 def CrossJointParameters():
834 CrossJointParameters = [
835 "radius",
836 "div",
837 "angle1",
838 "angle2",
839 "angle3",
840 "startLength",
841 "branch1Length",
842 "branch2Length",
843 "branch3Length",
845 return CrossJointParameters
847 class AddCrossJoint(Operator, object_utils.AddObjectHelper):
848 bl_idname = "mesh.primitive_cross_joint_add"
849 bl_label = "Add Pipe Cross-Joint"
850 bl_description = "Construct a cross-joint pipe mesh"
851 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
853 CrossJoint : BoolProperty(name = "CrossJoint",
854 default = True,
855 description = "CrossJoint")
857 #### change properties
858 change : BoolProperty(name = "Change",
859 default = False,
860 description = "change CrossJoint")
862 radius: FloatProperty(
863 name="Radius",
864 description="The radius of the pipe",
865 default=1.0,
866 min=0.01,
867 max=100.0,
868 unit="LENGTH"
870 div: IntProperty(
871 name="Divisions",
872 description="Number of vertices (divisions)",
873 default=32,
874 min=4,
875 max=256
877 angle1: FloatProperty(
878 name="Angle 1",
879 description="The angle of the 1. arm (from the main axis)",
880 default=radians(90.0),
881 min=radians(-179.9),
882 max=radians(179.9),
883 unit="ROTATION"
885 angle2: FloatProperty(name="Angle 2",
886 description="The angle of the 2. arm (from the main axis)",
887 default=radians(90.0),
888 min=radians(-179.9),
889 max=radians(179.9),
890 unit="ROTATION"
892 angle3: FloatProperty(name="Angle 3 (center)",
893 description="The angle of the center arm (from the main axis)",
894 default=radians(0.0),
895 min=radians(-179.9),
896 max=radians(179.9),
897 unit="ROTATION"
899 startLength: FloatProperty(
900 name="Length Start",
901 description="Length of the beginning of the "
902 "main pipe (the straight one)",
903 default=3.0,
904 min=0.01,
905 max=100.0,
906 unit="LENGTH"
908 branch1Length: FloatProperty(name="Length Arm 1",
909 description="Length of the 1. arm",
910 default=3.0,
911 min=0.01,
912 max=100.0,
913 unit="LENGTH"
915 branch2Length: FloatProperty(
916 name="Length Arm 2",
917 description="Length of the 2. arm",
918 default=3.0,
919 min=0.01,
920 max=100.0,
921 unit="LENGTH"
923 branch3Length: FloatProperty(
924 name="Length Arm 3 (center)",
925 description="Length of the center arm",
926 default=3.0,
927 min=0.01,
928 max=100.0,
929 unit="LENGTH"
932 def draw(self, context):
933 layout = self.layout
935 box = layout.box()
936 box.prop(self, 'radius')
937 box.prop(self, 'div')
938 box.prop(self, 'angle1')
939 box.prop(self, 'angle2')
940 box.prop(self, 'angle3')
941 box.prop(self, 'startLength')
942 box.prop(self, 'branch1Length')
943 box.prop(self, 'branch2Length')
944 box.prop(self, 'branch3Length')
946 if self.change == False:
947 # generic transform props
948 box = layout.box()
949 box.prop(self, 'align', expand=True)
950 box.prop(self, 'location', expand=True)
951 box.prop(self, 'rotation', expand=True)
953 def execute(self, context):
954 # turn off 'Enter Edit Mode'
955 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
956 bpy.context.preferences.edit.use_enter_edit_mode = False
958 radius = self.radius
959 div = self.div
961 angle1 = self.angle1
962 angle2 = self.angle2
963 angle3 = self.angle3
965 startLength = self.startLength
966 branch1Length = self.branch1Length
967 branch2Length = self.branch2Length
968 branch3Length = self.branch3Length
969 if (div % 2):
970 # Odd vertice number not supported (yet)
971 self.report({'INFO'}, "Odd vertices number is not yet supported")
972 return {'CANCELLED'}
974 verts = []
975 faces = []
977 # List of vert indices of each cross section
978 loopMainStart = [] # Vert indices for the beginning of the main pipe
979 loopJoint1 = [] # Vert index for joint that is used to connect the joint & loopMainStart
980 loopJoint2 = [] # Vert index for joint that is used to connect the joint & loopArm1
981 loopJoint3 = [] # Vert index for joint that is used to connect the joint & loopArm2
982 loopJoint4 = [] # Vert index for joint that is used to connect the joint & loopArm3
983 loopArm1 = [] # Vert idxs for the end of the 1. arm
984 loopArm2 = [] # Vert idxs for the end of the 2. arm
985 loopArm3 = [] # Vert idxs for the center arm end
987 # Create start circle
988 for vertIdx in range(div):
989 curVertAngle = vertIdx * (2.0 * pi / div)
990 locX = sin(curVertAngle)
991 locY = cos(curVertAngle)
992 locZ = -startLength
993 loopMainStart.append(len(verts))
994 verts.append([locX * radius, locY * radius, locZ])
996 # Create 1. deformed joint circle
997 vertTemp1 = None
998 vertTemp2 = None
999 for vertIdx in range(div):
1000 curVertAngle = vertIdx * (2.0 * pi / div)
1001 locX = sin(curVertAngle)
1002 locY = cos(curVertAngle)
1004 if vertIdx == 0:
1005 vertTemp2 = len(verts)
1006 if vertIdx == div / 2:
1007 # @todo: This will possibly break if we
1008 # ever support odd divisions.
1009 vertTemp1 = len(verts)
1011 loopJoint1.append(len(verts))
1012 if (vertIdx > div / 2):
1013 locZ = locX * tan(angle1 / 2.0)
1014 loopJoint2.append(len(verts))
1015 else:
1016 locZ = locX * tan(-angle2 / 2.0)
1017 loopJoint3.append(len(verts))
1019 verts.append([locX * radius, locY * radius, locZ * radius])
1021 # Create 2. deformed joint circle
1022 loopTempA = []
1023 loopTempB = []
1024 angleJoint1 = (angle1 - angle3) / 2.0
1025 angleJoint2 = (angle2 + angle3) / 2.0
1026 for vertIdx in range(div):
1027 curVertAngle = vertIdx * (2.0 * pi / div)
1029 # Skip pole vertices
1030 # @todo: This will possibly break if
1031 # we ever support odd divisions
1032 if not (vertIdx == 0) and not (vertIdx == div / 2):
1034 if (vertIdx > div / 2):
1035 angleJoint = angleJoint1
1036 angle = angle1
1037 Z = -1.0
1038 loopTempA.append(len(verts))
1040 else:
1041 angleJoint = angleJoint2
1042 angle = angle2
1043 Z = 1.0
1044 loopTempB.append(len(verts))
1046 locX = (sin(curVertAngle) * sin(angleJoint) / sin(angle - angleJoint))
1047 locY = -cos(curVertAngle)
1048 locZ = (Z * (sin(curVertAngle) * cos(angleJoint) / sin(angle - angleJoint)))
1050 verts.append([locX * radius, locY * radius, locZ * radius])
1052 loopTempA2 = loopTempA[:]
1053 loopTempB2 = loopTempB[:]
1054 loopTempB3 = loopTempB[:]
1056 # Finalise 2. loop
1057 loopTempA.append(vertTemp1)
1058 loopTempA.reverse()
1059 loopTempA.append(vertTemp2)
1060 loopJoint2.reverse()
1061 loopJoint2.extend(loopTempA)
1062 loopJoint2.reverse()
1064 # Finalise 3. loop
1065 loopJoint3.extend(loopTempB3)
1067 # Finalise 4. loop
1068 loopTempA2.append(vertTemp1)
1069 loopTempA2.reverse()
1070 loopTempB2.append(vertTemp2)
1071 loopJoint4.extend(reversed(loopTempB2))
1072 loopJoint4.extend(loopTempA2)
1074 # Create end circle (1. branching pipe)
1075 baseEndLocX = -branch1Length * sin(angle1)
1076 baseEndLocZ = branch1Length * cos(angle1)
1077 for vertIdx in range(div):
1078 curVertAngle = vertIdx * (2.0 * pi / div)
1079 # Create circle
1080 locX = sin(curVertAngle) * radius
1081 locY = cos(curVertAngle) * radius
1082 locZ = 0.0
1084 # Rotate circle
1085 locZ = locX * cos(pi / 2.0 - angle1)
1086 locX = locX * sin(pi / 2.0 - angle1)
1088 loopArm1.append(len(verts))
1089 # Add translated circle.
1090 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1092 # Create end circle (2. branching pipe)
1093 baseEndLocX = branch2Length * sin(angle2)
1094 baseEndLocZ = branch2Length * cos(angle2)
1095 for vertIdx in range(div):
1096 curVertAngle = vertIdx * (2.0 * pi / div)
1097 # Create circle
1098 locX = sin(curVertAngle) * radius
1099 locY = cos(curVertAngle) * radius
1100 locZ = 0.0
1102 # Rotate circle
1103 locZ = locX * cos(pi / 2.0 + angle2)
1104 locX = locX * sin(pi / 2.0 + angle2)
1106 loopArm2.append(len(verts))
1107 # Add translated circle
1108 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1110 # Create end circle (center pipe)
1111 baseEndLocX = branch3Length * sin(angle3)
1112 baseEndLocZ = branch3Length * cos(angle3)
1113 for vertIdx in range(div):
1114 curVertAngle = vertIdx * (2.0 * pi / div)
1115 # Create circle
1116 locX = sin(curVertAngle) * radius
1117 locY = cos(curVertAngle) * radius
1118 locZ = 0.0
1120 # Rotate circle
1121 locZ = locX * cos(pi / 2.0 + angle3)
1122 locX = locX * sin(pi / 2.0 + angle3)
1124 loopArm3.append(len(verts))
1125 # Add translated circle
1126 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1128 # Create faces
1129 faces.extend(createFaces(loopMainStart, loopJoint1, closed=True))
1130 faces.extend(createFaces(loopJoint2, loopArm1, closed=True))
1131 faces.extend(createFaces(loopJoint3, loopArm2, closed=True))
1132 faces.extend(createFaces(loopJoint4, loopArm3, closed=True))
1134 if bpy.context.mode == "OBJECT":
1135 if (context.selected_objects != []) and context.active_object and \
1136 (context.active_object.data is not None) and ('CrossJoint' in context.active_object.data.keys()) and \
1137 (self.change == True):
1138 obj = context.active_object
1139 oldmesh = obj.data
1140 oldmeshname = obj.data.name
1141 mesh = create_mesh(context, verts, [], faces, "Cross Joint")
1142 obj.data = mesh
1143 for material in oldmesh.materials:
1144 obj.data.materials.append(material)
1145 bpy.data.meshes.remove(oldmesh)
1146 obj.data.name = oldmeshname
1147 else:
1148 mesh = create_mesh(context, verts, [], faces, "Cross Joint")
1149 obj = object_utils.object_data_add(context, mesh, operator=self)
1151 mesh.update()
1153 obj.data["CrossJoint"] = True
1154 obj.data["change"] = False
1155 for prm in CrossJointParameters():
1156 obj.data[prm] = getattr(self, prm)
1158 if bpy.context.mode == "EDIT_MESH":
1159 active_object = context.active_object
1160 name_active_object = active_object.name
1161 bpy.ops.object.mode_set(mode='OBJECT')
1162 mesh = create_mesh(context, verts, [], faces, "TMP")
1163 obj = object_utils.object_data_add(context, mesh, operator=self)
1164 obj.select_set(True)
1165 active_object.select_set(True)
1166 bpy.context.view_layer.objects.active = active_object
1167 bpy.ops.object.join()
1168 context.active_object.name = name_active_object
1169 bpy.ops.object.mode_set(mode='EDIT')
1171 if use_enter_edit_mode:
1172 bpy.ops.object.mode_set(mode = 'EDIT')
1174 # restore pre operator state
1175 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
1177 return {'FINISHED'}
1180 # Create the vertices and polygons for a regular n-joint
1181 def NJointParameters():
1182 NJointParameters = [
1183 "radius",
1184 "div",
1185 "number",
1186 "length",
1188 return NJointParameters
1190 class AddNJoint(Operator, object_utils.AddObjectHelper):
1191 bl_idname = "mesh.primitive_n_joint_add"
1192 bl_label = "Add Pipe N-Joint"
1193 bl_description = "Construct a n-joint pipe mesh"
1194 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
1196 NJoint : BoolProperty(name = "NJoint",
1197 default = True,
1198 description = "NJoint")
1200 #### change properties
1201 change : BoolProperty(name = "Change",
1202 default = False,
1203 description = "change NJoint")
1205 radius: FloatProperty(
1206 name="Radius",
1207 description="The radius of the pipe",
1208 default=1.0,
1209 min=0.01,
1210 max=100.0,
1211 unit="LENGTH"
1213 div: IntProperty(
1214 name="Divisions",
1215 description="Number of vertices (divisions)",
1216 default=32,
1217 min=4,
1218 max=256
1220 number: IntProperty(
1221 name="Arms / Joints",
1222 description="Number of joints / arms",
1223 default=5,
1224 min=2,
1225 max=99999
1227 length: FloatProperty(
1228 name="Length",
1229 description="Length of each joint / arm",
1230 default=3.0,
1231 min=0.01,
1232 max=100.0,
1233 unit="LENGTH"
1236 def draw(self, context):
1237 layout = self.layout
1239 box = layout.box()
1240 box.prop(self, 'radius')
1241 box.prop(self, 'div')
1242 box.prop(self, 'number')
1243 box.prop(self, 'length')
1245 if self.change == False:
1246 # generic transform props
1247 box = layout.box()
1248 box.prop(self, 'align', expand=True)
1249 box.prop(self, 'location', expand=True)
1250 box.prop(self, 'rotation', expand=True)
1252 def execute(self, context):
1253 # turn off 'Enter Edit Mode'
1254 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
1255 bpy.context.preferences.edit.use_enter_edit_mode = False
1257 radius = self.radius
1258 div = self.div
1259 number = self.number
1260 length = self.length
1262 if (div % 2):
1263 # Odd vertice number not supported (yet)
1264 self.report({'INFO'}, "Odd vertices number is not yet supported")
1265 return {'CANCELLED'}
1267 if (number < 2):
1268 return {'CANCELLED'}
1270 verts = []
1271 faces = []
1273 loopsEndCircles = []
1274 loopsJointsTemp = []
1275 loopsJoints = []
1277 vertTemp1 = None
1278 vertTemp2 = None
1280 angleDiv = (2.0 * pi / number)
1282 # Create vertices for the end circles
1283 for num in range(number):
1284 circle = []
1285 # Create start circle
1286 angle = num * angleDiv
1288 baseEndLocX = length * sin(angle)
1289 baseEndLocZ = length * cos(angle)
1290 for vertIdx in range(div):
1291 curVertAngle = vertIdx * (2.0 * pi / div)
1292 # Create circle
1293 locX = sin(curVertAngle) * radius
1294 locY = cos(curVertAngle) * radius
1295 locZ = 0.0
1297 # Rotate circle
1298 locZ = locX * cos(pi / 2.0 + angle)
1299 locX = locX * sin(pi / 2.0 + angle)
1301 circle.append(len(verts))
1302 # Add translated circle
1303 verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ])
1305 loopsEndCircles.append(circle)
1307 # Create vertices for the joint circles
1308 loopJoint = []
1309 for vertIdx in range(div):
1310 curVertAngle = vertIdx * (2.0 * pi / div)
1311 locX = sin(curVertAngle)
1312 locY = cos(curVertAngle)
1314 skipVert = False
1315 # Store pole vertices
1316 if vertIdx == 0:
1317 if (num == 0):
1318 vertTemp2 = len(verts)
1319 else:
1320 skipVert = True
1321 elif vertIdx == div / 2:
1322 # @todo: This will possibly break if we
1323 # ever support odd divisions
1324 if (num == 0):
1325 vertTemp1 = len(verts)
1326 else:
1327 skipVert = True
1329 if not skipVert:
1330 if (vertIdx > div / 2):
1331 locZ = -locX * tan((pi - angleDiv) / 2.0)
1332 loopJoint.append(len(verts))
1334 # Rotate the vert
1335 cosAng = cos(-angle)
1336 sinAng = sin(-angle)
1337 LocXnew = locX * cosAng - locZ * sinAng
1338 LocZnew = locZ * cosAng + locX * sinAng
1339 locZ = LocZnew
1340 locX = LocXnew
1342 verts.append([
1343 locX * radius,
1344 locY * radius,
1345 locZ * radius])
1346 else:
1347 # These two vertices will only be
1348 # added the very first time.
1349 if vertIdx == 0 or vertIdx == div / 2:
1350 verts.append([locX * radius, locY * radius, locZ])
1352 loopsJointsTemp.append(loopJoint)
1354 # Create complete loops (loopsJoints) out of the
1355 # double number of half loops in loopsJointsTemp
1356 for halfLoopIdx in range(len(loopsJointsTemp)):
1357 if (halfLoopIdx == len(loopsJointsTemp) - 1):
1358 idx1 = halfLoopIdx
1359 idx2 = 0
1360 else:
1361 idx1 = halfLoopIdx
1362 idx2 = halfLoopIdx + 1
1364 loopJoint = []
1365 loopJoint.append(vertTemp2)
1366 loopJoint.extend(reversed(loopsJointsTemp[idx2]))
1367 loopJoint.append(vertTemp1)
1368 loopJoint.extend(loopsJointsTemp[idx1])
1370 loopsJoints.append(loopJoint)
1372 # Create faces from the two
1373 # loop arrays (loopsJoints -> loopsEndCircles)
1374 for loopIdx in range(len(loopsEndCircles)):
1375 faces.extend(
1376 createFaces(loopsJoints[loopIdx],
1377 loopsEndCircles[loopIdx], closed=True))
1379 if bpy.context.mode == "OBJECT":
1380 if (context.selected_objects != []) and context.active_object and \
1381 (context.active_object.data is not None) and ('NJoint' in context.active_object.data.keys()) and \
1382 (self.change == True):
1383 obj = context.active_object
1384 oldmesh = obj.data
1385 oldmeshname = obj.data.name
1386 mesh = create_mesh(context, verts, [], faces, "N Joint")
1387 obj.data = mesh
1388 for material in oldmesh.materials:
1389 obj.data.materials.append(material)
1390 bpy.data.meshes.remove(oldmesh)
1391 obj.data.name = oldmeshname
1392 else:
1393 mesh = create_mesh(context, verts, [], faces, "N Joint")
1394 obj = object_utils.object_data_add(context, mesh, operator=self)
1396 obj.data["NJoint"] = True
1397 obj.data["change"] = False
1398 for prm in NJointParameters():
1399 obj.data[prm] = getattr(self, prm)
1401 if bpy.context.mode == "EDIT_MESH":
1402 active_object = context.active_object
1403 name_active_object = active_object.name
1404 bpy.ops.object.mode_set(mode='OBJECT')
1405 mesh = create_mesh(context, verts, [], faces, "TMP")
1406 obj = object_utils.object_data_add(context, mesh, operator=self)
1407 obj.select_set(True)
1408 active_object.select_set(True)
1409 bpy.context.view_layer.objects.active = active_object
1410 bpy.ops.object.join()
1411 context.active_object.name = name_active_object
1412 bpy.ops.object.mode_set(mode='EDIT')
1414 if use_enter_edit_mode:
1415 bpy.ops.object.mode_set(mode = 'EDIT')
1417 # restore pre operator state
1418 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
1420 return {'FINISHED'}