20 | Only dimensions used are:
21 | M, M1, M2, G, G1, G2, H, H1, H2, POD, POD1, POD2, PThk, PThk1, PThk2.
22 | All other dimensions are used for inromation.
23 |
20 | To construct an elbow only these dimensions are used:
21 | BendingAngle, H, J, M, POD, and PThk.
22 | In Additinon, Dodo/Flamingo uses the Schedule dimension if it is present in the table.
23 | All other dimensions are used for inromation only.
24 |
20 | To construct an cross only these dimensions are used:
21 | G, G1, H, H1, L, L1, M, M1, POD, POD1, PThk, Pthk1.
22 | In Additinon, Flamingo uses the Schedule dimension if it is present in the table.
23 | All other dimensions are used for inromation only.
24 |
20 | To construct a coupling we use these dimensions, elbow only these dimensions are used:
21 | alpha, L, N, M, M1, POD, POD1, PThk, and PThk1.
22 | In Additinon, Flamingo uses the Schedule dimension if it is present in the table.
23 | All other dimensions are used for inromation only.
24 |
22 | To construct a part, only these dimensions are used:
23 | OD, Thk and the pipe height (length).
24 | Dodo/Flamingo also uses Schedule and DN if they are present in the table. All other dimensions are used for inromation.
25 |
"""
26 | params.keyColumnName = "PartNumber"
27 | super(MainDialog, self).__init__(params)
28 |
29 | def createLengthWidget(self, Dialog):
30 | self.widgetLengthInput = QtGui.QWidget(Dialog)
31 | self.widgetLengthInput.setMinimumSize(QtCore.QSize(0, 27))
32 | self.widgetLengthInput.setObjectName("widgetLengthInput")
33 | self.labelHeight = QtGui.QLabel(self.widgetLengthInput)
34 | self.labelHeight.setGeometry(QtCore.QRect(0, 0, 121, 27))
35 | sizePolicy = QtGui.QSizePolicy(
36 | QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum)
37 | sizePolicy.setHorizontalStretch(0)
38 | sizePolicy.setVerticalStretch(0)
39 | sizePolicy.setHeightForWidth(
40 | self.labelHeight.sizePolicy().hasHeightForWidth())
41 | self.labelHeight.setSizePolicy(sizePolicy)
42 | self.labelHeight.setMinimumSize(QtCore.QSize(0, 27))
43 | self.labelHeight.setMaximumSize(QtCore.QSize(200, 16777215))
44 | self.labelHeight.setObjectName("labelHeight")
45 | self.lineEditLength = QtGui.QLineEdit(self.widgetLengthInput)
46 | self.lineEditLength.setGeometry(QtCore.QRect(120, 0, 91, 27))
47 | self.lineEditLength.setObjectName("lineEditLength")
48 | # Add text.
49 | self.labelHeight.setText(QtGui.QApplication.translate(
50 | "Dialog", "Height (Length):", None, CreatePartGui.UnicodeUTF8()))
51 | self.lineEditLength.setText(QtGui.QApplication.translate(
52 | "Dialog", "1 m", None, CreatePartGui.UnicodeUTF8()))
53 | return self.widgetLengthInput
54 |
55 | # Add customized UI elements after the type. That is the length ui
56 | def setupUi(self, Dialog):
57 | super(MainDialog, self).setupUi(Dialog)
58 | # Append UI for pipe length input after the output type block.
59 | after_index = self.verticalLayout.indexOf(self.outputTypeWidget) + 1
60 | widget = self.createLengthWidget(Dialog)
61 | self.verticalLayout.insertWidget(after_index, widget)
62 |
63 | def saveAdditionalData(self, settings):
64 | settings.setValue("lineEditLength", self.lineEditLength.text())
65 |
66 | def restoreAdditionalInput(self, settings):
67 | text = settings.value("lineEditLength")
68 | if text is not None:
69 | self.lineEditLength.setText(text)
70 |
71 | def createNewPart(self, document, table, partName, outputType):
72 | length = FreeCAD.Units.parseQuantity(self.lineEditLength.text())
73 | builder = Pipe.PipeFromTable(self.params.document, self.params.table)
74 | part = builder.create(partName, length, outputType)
75 | if outputType == Piping.OUTPUT_TYPE_DODO_OR_FLAMINGO:
76 | self.moveFlamingoPartToSelection(document, part)
77 | return part
78 |
79 |
80 | def GuiCheckTable():
81 | return CreatePartGui.GuiCheckTable(Pipe.CSV_TABLE_PATH, Pipe.DIMENSIONS_USED)
82 |
--------------------------------------------------------------------------------
/InitGui.py:
--------------------------------------------------------------------------------
1 | # ***************************************************************************
2 | # * *
3 | # * This file is part of the OSE project. *
4 | # * *
5 | # * *
6 | # * Copyright (C) 2017 *
7 | # * Ruslan Krenzler *
8 | # * Stephen Kaiser *
9 | # * *
10 | # * This library is free software; you can redistribute it and/or *
11 | # * modify it under the terms of the GNU Lesser General Public *
12 | # * License as published by the Free Software Foundation; either *
13 | # * version 2 of the License, or (at your option) any later version. *
14 | # * *
15 | # * This library is distributed in the hope that it will be useful, *
16 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
18 | # * Lesser General Public License for more details. *
19 | # * *
20 | # * You should have received a copy of the GNU Lesser General Public *
21 | # * License along with this library; if not, If not, see *
22 | # * . *
23 | # * *
24 | # * *
25 | # ***************************************************************************
26 |
27 |
28 | class OsePipingWorkbench (Workbench):
29 |
30 | MenuText = "OSE Piping Workbench"
31 | ToolTip = "A piping workbench for Open Source Ecology part design"
32 |
33 | def __init__(self):
34 | # This is the only place, where I could initialize the workbach icon.
35 | import os
36 | import OsePipingBase
37 | self.__class__.Icon = os.path.join(
38 | OsePipingBase.ICON_PATH, "Workbench.svg")
39 |
40 | def Initialize(self):
41 | "This function is executed when FreeCAD starts"
42 | # Test try to load other modules
43 | import OsePipingBase
44 | # import here all the needed files that create your FreeCAD commands
45 | import OsePipingCommands
46 | self.partList = ["OsePiping_Pipe", "OsePiping_Coupling", "OsePiping_Bushing", "OsePiping_Elbow", "OsePiping_SweepElbow",
47 | "OsePiping_Tee", "OsePiping_Corner", "OsePiping_Cross"] # A list of command names created in the line above
48 | # creates a new toolbar with your commands
49 | self.appendToolbar("Ose Piping", self.partList)
50 | self.appendMenu("Command Menu", self.partList) # creates a new menu
51 |
52 | # Add commands to connect and rotate parts.
53 | self.modificationList = ["OsePiping_MoveAround", "OsePiping_MoveTo"]
54 | self.appendToolbar("Modification", self.modificationList)
55 | self.appendMenu("Modification", self.modificationList) # creates a new menu
56 | #OSE_PipingWorkbench.Icon = os.path.join(OSEBase.ICON_PATH,"Workbench.svg")
57 |
58 | # FreeCADGui.addIconPath(":/Resources/icons")
59 | # FreeCADGui.addLanguagePath(":/translations")
60 | # FreeCADGui.addPreferencePage(":/ui/preferences-ose.ui","OSE")
61 | # FreeCADGui.addPreferencePage(":/ui/preferences-osedefaults.ui","OSE")
62 | # self.appendMenu(["An existing Menu", "My submenu"], self.list) # appends a submenu to an existing menu
63 |
64 | def Activated(self):
65 | "This function is executed when the workbench is activated"
66 | return
67 |
68 | def Deactivated(self):
69 | "This function is executed when the workbench is deactivated"
70 | return
71 |
72 | def ContextMenu(self, recipient):
73 | "This is executed whenever the user right-clicks on screen"
74 | # "recipient" will be either "view" or "tree"
75 | # add commands to the context menu
76 | self.appendContextMenu("Piping commands", self.partList)
77 |
78 | def GetClassName(self):
79 | # this function is mandatory if this is a full python workbench
80 | return "Gui::PythonWorkbench"
81 |
82 |
83 | Gui.addWorkbench(OsePipingWorkbench())
84 |
--------------------------------------------------------------------------------
/OsePiping/Port.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Author: Ruslan Krenzler.
3 | # Date: 20 November 2018
4 | # Advanced Ports. Ports with normal vector and rotation references.
5 |
6 | import FreeCAD
7 |
8 |
9 | class AdvancedPort:
10 | def __init__(self, base=None, rotation=None):
11 |
12 | if base is None:
13 | base = FreeCAD.Vector(0, 0, 0)
14 |
15 | if rotation is None:
16 | rotation = FreeCAD.Rotation(0, 0, 0)
17 |
18 | self.placement = FreeCAD.Placement(base, rotation)
19 |
20 | def getNormal(self):
21 | return self.placement.Rotation.multVec(FreeCAD.Vector(1, 0, 0))
22 |
23 | def getAngleReference(self):
24 | return self.placement.Rotation.multVec(FreeCAD.Vector(0, 1, 0))
25 |
26 | def getPartRotation(self, other_placement, other_port):
27 | """Return a rotation matrix wihch will rotate this port to the other port.
28 |
29 | param other_placement:
30 | param other_port: other pOrt.
31 |
32 | """
33 | # Note: Multiplication of rotation matrices is commutative!
34 | # Apply operations from right to left.
35 | # Rotat itself back, such that normal points to x axis and angle reference
36 | # r points to y axis.
37 | A_inv = self.placement.Rotation.inverted()
38 | # print("A_inv " + str(A_inv.toEuler()))
39 | # Rotate the port such that the x axis shows back, but the angle reference
40 | # coinsides with previous one.
41 | A_r = FreeCAD.Rotation(0, 180, 0)
42 | # print("A_r " + str(A_r.toEuler()))
43 | A = other_port.placement.Rotation
44 | # print("A " + str(A.toEuler()))
45 | R = other_placement.Rotation
46 | # print("R " + str(R.toEuler()))
47 | B = R.multiply(A.multiply(A_r.multiply(A_inv)))
48 | # print("B " + str(B.toEuler()))
49 | return B
50 |
51 | def getPartBase(self, other_placement, other_port):
52 | # Check find first the global bosition of the other portself.
53 | other_g_base = other_placement.Base + \
54 | other_placement.Rotation.multVec(other_port.placement.Base)
55 | # Get new rotation.
56 | B = self.getPartRotation(other_placement, other_port)
57 | # Get new port position taking in account the adjusting rotation.
58 | adjusted_base = B.multVec(self.placement.Base)
59 | return other_g_base - adjusted_base
60 |
61 | def getPartPlacement(self, other_placement, other_port):
62 | """Return new part placment adjusted to the port of the other part."""
63 | return FreeCAD.Placement(self.getPartBase(other_placement, other_port=other_port),
64 | self.getPartRotation(other_placement, other_port=other_port))
65 |
66 |
67 | def testPorts():
68 | # Port 0 in a tee.
69 | port1 = AdvancedPort(FreeCAD.Vector(0, 0, 2), FreeCAD.Rotation(0, -90, 0))
70 | # Port 2 in a tee.
71 | port2 = AdvancedPort(FreeCAD.Vector(-2, 0, 0),
72 | FreeCAD.Rotation(180, 0, 180))
73 | part_placement = FreeCAD.Placement()
74 | # part_placement = FreeCAD.Placement(FreeCAD.Vector(0, 1, 1), FreeCAD.Rotation(0, 0, 0))
75 |
76 | print(port1.placement)
77 | print(port2.placement)
78 |
79 | print(port2.getPartPlacement(part_placement, port1))
80 |
81 |
82 | def supportsAdvancedPort(part):
83 | """Check if the part contains advanced ports."""
84 | if part is None:
85 | return False
86 | if hasattr(part, "PortRotationAngles"):
87 | return True # This is an OSE-fitting.
88 | if hasattr(part, "PType") and part.PType == u"Pipe":
89 | return True # This is a Flamingo pipe.
90 | return False # This is something else.
91 |
92 |
93 | def _guessPipeAdvancedPorts(part):
94 | """Return port advanced port of a vertical streight flamingo pipe."""
95 | port_bottom = AdvancedPort(base=FreeCAD.Vector(
96 | part.Ports[0]), rotation=FreeCAD.Rotation(0, 90, 0))
97 | port_top = AdvancedPort(base=FreeCAD.Vector(
98 | part.Ports[1]), rotation=FreeCAD.Rotation(0, -90, 0))
99 | return [port_bottom, port_top]
100 |
101 |
102 | def _extractAdvancedPorts(part):
103 | """Extract advanced ports from a FeaturePython part."""
104 | res = []
105 | for i in range(0, len(part.Ports)):
106 | rotation_angles = part.PortRotationAngles[i]
107 | rotation = FreeCAD.Rotation(
108 | rotation_angles.x, rotation_angles.y, rotation_angles.z)
109 | port = AdvancedPort(base=FreeCAD.Vector(
110 | part.Ports[i]), rotation=rotation)
111 | res.append(port)
112 | return res
113 |
114 |
115 | def extractAdvancedPorts(part):
116 | if part.PType == u"Pipe":
117 | return _guessPipeAdvancedPorts(part)
118 | else:
119 | return _extractAdvancedPorts(part)
120 |
121 |
122 | def getNearestPort(part_placement, ports, point):
123 | d_so_far = float("inf")
124 | closest_port = None
125 | for port in ports:
126 | global_pos = part_placement.Base + \
127 | part_placement.Rotation.multVec(port.placement.Base)
128 | d = global_pos.distanceToPoint(point)
129 | if d < d_so_far:
130 | d_so_far = d
131 | closest_port = port
132 | return closest_port
133 |
134 |
135 | def getNearestPortIndex(part_placement, ports, point):
136 | closest_port = getNearestPort(part_placement, ports, point)
137 | return ports.index(closest_port)
138 |
139 | # testPorts()
140 |
--------------------------------------------------------------------------------
/ui/create-part.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 800
10 | 800
11 |
12 |
13 |
14 | Dialog Title
15 |
16 |
17 |
18 |
19 |
20 | true
21 |
22 |
23 |
24 | 75
25 | true
26 |
27 |
28 |
29 | false
30 |
31 |
32 | false
33 |
34 |
35 | <html><head/><body><p><span style=" color:#ef2929;">The Flamingo-Workbench is deprecated, please install Dodo instead.</span></p></body></html>
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 0
44 | 55
45 |
46 |
47 |
48 | Qt::LeftToRight
49 |
50 |
51 |
52 |
53 | 10
54 | 0
55 | 301
56 | 61
57 |
58 |
59 |
60 | Output type:
61 |
62 |
63 |
64 |
65 |
66 | true
67 |
68 |
69 | Solid
70 |
71 |
72 | true
73 |
74 |
75 |
76 |
77 |
78 |
79 | false
80 |
81 |
82 | Dodo/Flamingo
83 |
84 |
85 | false
86 |
87 |
88 |
89 |
90 |
91 |
92 | Parts
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | QAbstractItemView::SingleSelection
104 |
105 |
106 | QAbstractItemView::SelectRows
107 |
108 |
109 |
110 |
111 |
112 |
113 | <html><head/><body><p>Information about dimensions</p></body></html>
114 |
115 |
116 | Qt::AutoText
117 |
118 |
119 | true
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | ../Resources/images/coupling-dimensions.png
130 |
131 |
132 | Qt::AlignCenter
133 |
134 |
135 |
136 |
137 |
138 |
139 | Qt::Horizontal
140 |
141 |
142 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | buttonBox
152 | accepted()
153 | Dialog
154 | accept()
155 |
156 |
157 | 248
158 | 254
159 |
160 |
161 | 157
162 | 274
163 |
164 |
165 |
166 |
167 | buttonBox
168 | rejected()
169 | Dialog
170 | reject()
171 |
172 |
173 | 316
174 | 260
175 |
176 |
177 | 286
178 | 274
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/tables/cross.csv:
--------------------------------------------------------------------------------
1 | "PartNumber","PipeSize","PSize","PipeSize1","PSize1","Schedule","G","G1","H","H1","L","L1","M","M1","Approx. Wt.","POD","PThk","PID","POD1","PThk1","PID1"
2 | "420-005","1/2""","DN15","1/2""","DN15",40,"17/32 in","17/32 in","1+9/32 in","1+9/32 in","2+17/32 in","2+17/32 in","1+3/32 in","1+3/32 in",".07 lb","0.840 in","0.109 in","0.602 in","0.840 in","0.109 in","0.602 in"
3 | "420-007","3/4""","DN20","3/4""","DN20",40,"9/16 in","9/16 in","1+9/16 in","1+9/16 in","3+1/8 in","3+1/8 in","1+5/16 in","1+5/16 in",".12 lb","1.050 in","0.113 in","0.804 in","1.050 in","0.113 in","0.804 in"
4 | "420-010","1""","DN25","1""","DN25",40,"13/16 in","13/16 in","1+15/16 in","1+15/16 in","3+7/8 in","3+7/8 in","1+5/8 in","1+5/8 in",".25 lb","1.315 in","0.133 in","1.029 in","1.315 in","0.133 in","1.029 in"
5 | "420-012","1-1/4""","DN32","1-1/4""","DN32",40,"1+3/32 in","1+3/32 in","2+11/32 in","2+11/32 in","4+23/32 in","4+23/32 in","2+1/16 in","2+1/16 in",".43 lb","1.660 in","0.140 in","1.360 in","1.660 in","0.140 in","1.360 in"
6 | "420-015","1-1/2""","DN40","1-1/2""","DN40",40,"1+1/4 in","1+1/4 in","2+19/32 in","2+19/32 in","5+5/32 in","5+5/32 in","2+5/16 in","2+5/16 in",".54 lb","1.900 in","0.145 in","1.590 in","1.900 in","0.145 in","1.590 in"
7 | "420-020","2""","DN50","2""","DN50",40,"1+17/32 in","1+17/32 in","2+29/32 in","2+29/32 in","5+27/32 in","5+27/32 in","2+13/16 in","2+13/16 in",".78 lb","2.375 in","0.154 in","2.047 in","2.375 in","0.154 in","2.047 in"
8 | "420-025","2-1/2""","DN65","2-1/2""","DN65",40,"1+21/32 in","1+21/32 in","3+21/32 in","3+21/32 in","7+11/32 in","7+11/32 in","3+3/8 in","3+3/8 in","1.61 lb","2.875 in","0.203 in","2.445 in","2.875 in","0.203 in","2.445 in"
9 | "420-030","3""","DN80","3""","DN80",40,"2+1/16 in","2+1/16 in","3+15/16 in","3+15/16 in","7+29/32 in","7+29/32 in","4+1/32 in","4+1/32 in","1.74 lb","3.500 in","0.216 in","3.042 in","3.500 in","0.216 in","3.042 in"
10 | "420-040","4""","DN100","4""","DN100",40,"2+1/2 in","2+1/2 in","4+1/2 in","4+1/2 in","9+1/32 in","9+1/32 in","5+1/32 in","5+1/32 in","3.22 lb","4.500 in","0.237 in","3.998 in","4.500 in","0.237 in","3.998 in"
11 | "420-050F","5""","DN125","5""","DN125",40,"6 in","6 in","9 in","9 in","18 in","18 in","6+1/16 in","6+1/16 in","13.68 lb","5.563 in","0.258 in","5.016 in","5.563 in","0.258 in","5.016 in"
12 | "420-060F","6""","DN150","6""","DN150",40,"6+3/16 in","6+3/16 in","9+7/16 in","9+7/16 in","18+7/8 in","18+7/8 in","7+3/16 in","7+3/16 in","12.36 lb","6.625 in","0.280 in","6.031 in","6.625 in","0.280 in","6.031 in"
13 | "420-080F","8""","DN200","8""","DN200",40,"8+9/16 in","8+9/16 in","12+13/16 in","12+13/16 in","25+5/8 in","25+5/8 in","9+1/4 in","9+1/4 in","26.10 lb","8.625 in","0.322 in","7.942 in","8.625 in","0.322 in","7.942 in"
14 | "420-100F","10""","DN250","10""","DN250",40,"9+7/8 in","9+3/8 in","15+1/8 in","14+5/8 in","30+1/4 in","29+1/4 in","11+1/2 in","11+1/2 in","43.81 lb","10.750 in","0.365 in","9.976 in","10.750 in","0.365 in","9.976 in"
15 | "420-120F","12""","DN300","12""","DN300",40,"10+3/4 in","10+3/16 in","17 in","16+7/16 in","34 in","32+7/8 in","13+9/16 in","13+9/16 in","66.22 lb","12.750 in","0.406 in","11.889 in","12.750 in","0.406 in","11.889 in"
16 | "820-002","1/4""","DN8","1/4""","DN8",80,"13/32 in","13/32 in","1+1/32 in","1+1/32 in","2+1/16 in","2+1/16 in","27/32 in","27/32 in",".05 lb","0.540 in","0.119 in","0.282 in","0.540 in","0.119 in","0.282 in"
17 | "820-005","1/2""","DN15","1/2""","DN15",80,"9/16 in","9/16 in","1+15/32 in","1+15/32 in","2+15/16 in","2+15/16 in","1+3/16 in","1+3/16 in",".13 lb","0.840 in","0.147 in","0.526 in","0.840 in","0.147 in","0.526 in"
18 | "820-007","3/4""","DN20","3/4""","DN20",80,"5/8 in","5/8 in","1+5/8 in","1+5/8 in","3+9/32 in","3+9/32 in","1+13/32 in","1+13/32 in",".22 lb","1.050 in","0.154 in","0.722 in","1.050 in","0.154 in","0.722 in"
19 | "820-010","1""","DN25","1""","DN25",80,"31/32 in","31/32 in","2+3/32 in","2+3/32 in","4+7/32 in","4+7/32 in","1+23/32 in","1+23/32 in",".39 lb","1.315 in","0.179 in","0.936 in","1.315 in","0.179 in","0.936 in"
20 | "820-012","1-1/4""","DN32","1-1/4""","DN32",80,"1+3/32 in","1+3/32 in","2+3/8 in","2+3/8 in","4+3/4 in","4+3/4 in","2+1/8 in","2+1/8 in",".63 lb","1.660 in","0.191 in","1.255 in","1.660 in","0.191 in","1.255 in"
21 | "820-015","1-1/2""","DN40","1-1/2""","DN40",80,"1+1/4 in","1+1/4 in","2+5/8 in","2+5/8 in","5+1/4 in","5+1/4 in","2+3/8 in","2+3/8 in",".80 lb","1.900 in","0.200 in","1.476 in","1.900 in","0.200 in","1.476 in"
22 | "820-020","2""","DN50","2""","DN50",80,"1+1/2 in","1+1/2 in","3 in","3 in","6 in","6 in","3 in","3 in","1.34 lb","2.375 in","0.218 in","1.913 in","2.375 in","0.218 in","1.913 in"
23 | "820-025","2-1/2""","DN65","2-1/2""","DN65",80,"1+11/16 in","1+11/16 in","3+15/32 in","3+15/32 in","6+29/32 in","6+29/32 in","3+17/32 in","3+17/32 in","2.02 lb","2.875 in","0.276 in","2.290 in","2.875 in","0.276 in","2.290 in"
24 | "820-030","3""","DN80","3""","DN80",80,"2+3/32 in","2+3/32 in","3+31/32 in","3+31/32 in","7+15/16 in","7+15/16 in","4+11/32 in","4+11/32 in","3.30 lb","3.500 in","0.300 in","2.864 in","3.500 in","0.300 in","2.864 in"
25 | "820-040","4""","DN100","4""","DN100",80,"2+5/8 in","2+5/8 in","4+7/8 in","4+7/8 in","9+3/4 in","9+3/4 in","5+11/32 in","5+11/32 in","5.31 lb","4.500 in","0.337 in","3.786 in","4.500 in","0.337 in","3.786 in"
26 | "820-050F","5""","DN125","5""","DN125",80,"5+1/2 in","5+1/2 in","8+1/2 in","8+1/2 in","17 in","17 in","6+5/16 in","6+5/16 in","14.69 lb","5.563 in","0.375 in","4.768 in","5.563 in","0.375 in","4.768 in"
27 | "820-060F","6""","DN150","6""","DN150",80,"6+1/4 in","6+1/4 in","9+1/2 in","9+1/2 in","19 in","19 in","7+1/2 in","7+1/2 in","16.19 lb","6.625 in","0.432 in","5.709 in","6.625 in","0.432 in","5.709 in"
28 | "820-080F","8""","DN200","8""","DN200",80,"7+5/8 in","7+5/8 in","11+7/8 in","11+7/8 in","23+3/4 in","23+3/4 in","9+5/8 in","9+5/8 in","28.22 lb","8.625 in","0.500 in","7.565 in","8.625 in","0.500 in","7.565 in"
29 | "820-100F","10""","DN250","10""","DN250",80,"9 in","9 in","14+1/4 in","14+1/4 in","28+1/2 in","28+1/2 in","11+15/16 in","11+15/16 in","49.78 lb","10.750 in","0.593 in","9.493 in","10.750 in","0.593 in","9.493 in"
30 | "820-120F","12""","DN300","12""","DN300",80,"11+1/4 in","11+1/4 in","17+1/2 in","17+1/2 in","35 in","35 in","14+1/8 in","14+1/8 in","80.83 lb","12.750 in","0.687 in","11.294 in","12.750 in","0.687 in","11.294 in"
--------------------------------------------------------------------------------
/Resources/icons/Workbench.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
176 |
--------------------------------------------------------------------------------
/Resources/icons/OSE_Piping_workbench_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
176 |
--------------------------------------------------------------------------------
/OsePiping/Piping.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Author: Ruslan Krenzler.
3 | # Date: 17 February 2018
4 | # General classes for pipe and fittng parts.
5 |
6 | import csv
7 | import Part
8 |
9 |
10 | class Error(Exception):
11 | """Base class for exceptions in this module."""
12 |
13 | def __init__(self, message):
14 | super(Error, self).__init__(message)
15 |
16 |
17 | class UnplausibleDimensions(Error):
18 | """Exception raised when dimensions are unplausible.
19 |
20 | For example if outer diameter is larger than the iner one.
21 | Attributes:
22 | message -- explanation of the error.
23 |
24 | """
25 |
26 | def __init__(self, message):
27 | super(UnplausibleDimensions, self).__init__(message)
28 |
29 |
30 | def nestedObjects(parent):
31 | """Return a list of a nested object contained in the parent parts.
32 |
33 | Children are added before the parents.
34 | """
35 | res = []
36 | if parent.OutList == []:
37 | res.append(parent)
38 | else:
39 | # Append children first.
40 | for o in parent.OutList:
41 | res += nestedObjects(o)
42 | res.append(parent)
43 | return res
44 |
45 |
46 | def removePartWithChildren(document, part):
47 | parts = nestedObjects(part)
48 | # Document.removeObjects can remove multple objects, when we use
49 | # parts directly. To prevent exceptions with deleted objects,
50 | # use the name list instead.
51 | names_to_remove = []
52 | for part in parts:
53 | if part.Name not in names_to_remove:
54 | names_to_remove.append(part.Name)
55 | for name in names_to_remove:
56 | # print("Deleting temporary objects %s." % name)
57 | document.removeObject(name)
58 |
59 |
60 | def toSolid(document, part, name):
61 | """Convert object to a solid.
62 |
63 | These are commands, which FreeCAD runs when a user converts a part to a solid.
64 | """
65 | s = part.Shape.Faces
66 | s = Part.Solid(Part.Shell(s))
67 | o = document.addObject("Part::Feature", name)
68 | o.Label = name
69 | o.Shape = s
70 | return o
71 |
72 |
73 | class CsvError(Error):
74 | """Base class for exceptions in this module."""
75 |
76 | def __init__(self, message):
77 | super(Error, self).__init__(message)
78 |
79 |
80 | class CsvTable:
81 | """Read pipe dimensions from a csv file.
82 |
83 | One part of the column must be unique and contains a unique key.
84 |
85 | Store the data as a list of rows. Each row is a list of values.
86 | """
87 |
88 | def __init__(self, mandatoryDims=None, keyColumnName="PartNumber"):
89 | """Initialize Class.
90 |
91 | @param mandatoryDims: list of column names which must be presented in the CSV files apart
92 | the "keyColumnName" column
93 | """
94 | self.headers = []
95 | self.data = []
96 | self.hasValidData = False
97 | if mandatoryDims is None:
98 | mandatoryDims = []
99 | self.mandatoryDims = mandatoryDims
100 | self._keyColumnName = keyColumnName
101 | self._keyColumnIndex = None
102 |
103 | def keyColumnName(self):
104 | return self._keyColumnName
105 |
106 | def load(self, filename):
107 | """Load data from a CSV file."""
108 | self.hasValidData = False
109 | with open(filename, "r") as csvfile:
110 | csv_reader = csv.reader(csvfile, delimiter=',', quotechar='"')
111 | self.headers = next(csv_reader)
112 | # Fill the talble
113 | self.data = []
114 | keys = []
115 | self._keyColumnIndex = self.headers.index(self._keyColumnName)
116 | for row in csv_reader:
117 | # Check if the keys is unique
118 | key = row[self._keyColumnIndex]
119 | if key in keys:
120 | msg = 'Error: Not unique key "%s" in column %s found in %s' % (
121 | key, self._keyColumnName, filename)
122 | raise CsvError(msg)
123 | else:
124 | keys.append(key)
125 | self.data.append(row)
126 | csvfile.close() # Should I close this file explicitely?
127 | self.hasValidData = self.hasNecessaryColumns()
128 |
129 | def hasNecessaryColumns(self):
130 | """Check if the data contains all the columns required to create a part."""
131 | return all(h in self.headers for h in (self.mandatoryDims + [self._keyColumnName]))
132 |
133 | def findPart(self, key):
134 | """Return first row with with key (part name) as a dictionary."""
135 | # Search for the first appearance of the name in this column.
136 | for row in self.data:
137 | if row[self._keyColumnIndex] == key:
138 | # Convert row to dicionary.
139 | return dict(zip(self.headers, row))
140 | return None
141 |
142 | def getPartKey(self, index):
143 | """Return part key of a row with the index *index*."""
144 | return self.data[index][self._keyColumnIndex]
145 |
146 |
147 | OUTPUT_TYPE_PARTS = 1
148 | OUTPUT_TYPE_SOLID = 2
149 | OUTPUT_TYPE_DODO_OR_FLAMINGO = 3
150 |
151 |
152 | def GetPressureRatingString(row):
153 | """Extract presssure rating string from a row of a fitting table.
154 |
155 | The Pressure rating has a form "SCH-[Schedule]". For example "SCH-40".
156 |
157 | :param row: a dictionary which contains non empty elements "Schedule" or "SCH".
158 | :return: String like "SCH-40" or "SCH-80".
159 | :return "": if there is no schedule data in the row.
160 | """
161 | if row.get("Schedule") is not None and len(row["Schedule"]) > 0:
162 | return "SCH-%s" % row["Schedule"]
163 | if row.get("SCH") is not None and len(row["SCH"]) > 0:
164 | return "SCH-%s" % row["SCH"]
165 | else:
166 | return "" # Nothing found
167 |
168 |
169 | def GetDnString(row):
170 | """Extract DN (diamètre nominal, nominal pipe size) string from a row of a fitting table.
171 |
172 | The Pressure rating has a form "DN[DN-Value]". For example "DN25".
173 |
174 | :param row: a dictionary which contains non empty elements "ND".
175 | :return: String like "DN10" or "DN25".
176 | :return "": if there is no DN data in the row.
177 | """
178 | if row.get("DN") is not None and len(row["DN"]) > 0:
179 | return "DN%s" % row["DN"]
180 | else:
181 | return "" # Nothing found
182 |
183 |
184 | def HasDodoSupport():
185 | import pkgutil
186 | mod = pkgutil.find_loader("pFeatures")
187 | return mod is not None
188 |
189 |
190 | def HasFlamingoSupport():
191 | import pkgutil
192 | mod = pkgutil.find_loader("pipeFeatures")
193 | return mod is not None
194 |
--------------------------------------------------------------------------------
/ui/create-pipe.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 836
10 | 633
11 |
12 |
13 |
14 | Create pipe
15 |
16 |
17 |
18 |
19 |
20 |
21 | 0
22 | 55
23 |
24 |
25 |
26 | Qt::LeftToRight
27 |
28 |
29 |
30 |
31 | 10
32 | 0
33 | 351
34 | 61
35 |
36 |
37 |
38 | Output type:
39 |
40 |
41 |
42 |
43 |
44 | true
45 |
46 |
47 | Solid
48 |
49 |
50 | true
51 |
52 |
53 |
54 |
55 |
56 |
57 | Parts
58 |
59 |
60 |
61 |
62 |
63 |
64 | false
65 |
66 |
67 | Flamingo (depricated)
68 |
69 |
70 | false
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | 0
83 | 27
84 |
85 |
86 |
87 |
88 |
89 | 0
90 | 0
91 | 121
92 | 27
93 |
94 |
95 |
96 |
97 | 0
98 | 0
99 |
100 |
101 |
102 |
103 | 0
104 | 27
105 |
106 |
107 |
108 |
109 | 200
110 | 16777215
111 |
112 |
113 |
114 | Height (Length):
115 |
116 |
117 |
118 |
119 |
120 | 120
121 | 0
122 | 91
123 | 27
124 |
125 |
126 |
127 | 1 m
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | 19
136 |
137 |
138 |
139 |
140 | QAbstractItemView::SingleSelection
141 |
142 |
143 | QAbstractItemView::SelectRows
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | ../Resources/images/pipe-dimensions.png
154 |
155 |
156 | Qt::AlignCenter
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | <html><head/><body><p>To construct a part, only these dimensions are used: OD, Thk and the pipe height (length). Flamingo also uses Schedule and DN if they are present in the table. All other dimensions are used for inromation.</p></body></html>
166 |
167 |
168 | Qt::AutoText
169 |
170 |
171 | true
172 |
173 |
174 |
175 |
176 |
177 |
178 | Qt::Horizontal
179 |
180 |
181 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
182 |
183 |
184 |
185 |
186 |
187 |
188 | radioButtonSolid
189 | radioButtonFlamingo
190 | lineEditLength
191 | buttonBox
192 |
193 |
194 |
195 |
196 | buttonBox
197 | accepted()
198 | Dialog
199 | accept()
200 |
201 |
202 | 248
203 | 254
204 |
205 |
206 | 157
207 | 274
208 |
209 |
210 |
211 |
212 | buttonBox
213 | rejected()
214 | Dialog
215 | reject()
216 |
217 |
218 | 316
219 | 260
220 |
221 |
222 | 286
223 | 274
224 |
225 |
226 |
227 |
228 |
229 |
--------------------------------------------------------------------------------
/OsePiping/Pipe.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Author: Ruslan Krenzler.
3 | # Date: 04 February 2018
4 | # Create a pipe.
5 |
6 | import os.path
7 | import FreeCAD
8 | import OsePipingBase
9 | import OsePiping.Piping as Piping
10 |
11 |
12 | parseQuantity = FreeCAD.Units.parseQuantity
13 |
14 | # This is the path to the dimensions table.
15 | CSV_TABLE_PATH = os.path.join(OsePipingBase.TABLE_PATH, 'pipe.csv')
16 | # It must contain unique values in the column "Name" and also, dimensions listened below.
17 | DIMENSIONS_USED = ["OD", "Thk"]
18 |
19 |
20 | # The value RELATIVE_EPSILON is used to slightly change the size of a subtracted part
21 | # to prevent problems with boolean operations.
22 | # This value does not change the appearance of part and can be large.
23 | # If the original value is L then we often use the value L*(1+RELATIVE_EPSILON) instead.
24 | # The relative deviation is then (L*(1+RELATIVE_EPSILON)-L)/L = RELATIVE_EPSILON.
25 | # That is why the constant has "relative" in its name.
26 | # On my version of freecad 0.16 The macro works even with RELATIVE_EPSILON = 0.0.
27 | # Maybe there is no more problems with boolean operations.
28 | RELATIVE_EPSILON = 0.1
29 |
30 |
31 | class Dimensions:
32 | def __init__(self):
33 | """Inititialize with test dimensions."""
34 | self.OD = parseQuantity("3 cm")
35 | self.Thk = parseQuantity("0.5 cm")
36 | self.H = parseQuantity("1 m")
37 |
38 | def isValid(self):
39 | errorMsg = ""
40 | if not (self.OD > 0):
41 | errorMsg = "OD (inner diameter) of the pipe must be positive. It is {} instead".format(
42 | self.OD)
43 | elif not (self.Thk <= self.OD / 2.0):
44 | errorMsg = "Pipe thickness Thk {} is too large: larger than OD/2 {}.".format(
45 | self.Thk, self.OD / 2.0)
46 | elif not (self.H > 0):
47 | errorMsg = "Height H={} must be positive".format(self.H)
48 |
49 | return (len(errorMsg) == 0, errorMsg)
50 |
51 | def ID(self):
52 | return self.OD - self.Thk * 2.0
53 |
54 |
55 | class Pipe:
56 | def __init__(self, document):
57 | self.document = document
58 | self.dims = Dimensions()
59 |
60 | def checkDimensions(self):
61 | valid, msg = self.dims.isValid()
62 | if not valid:
63 | raise Piping.UnplausibleDimensions(msg)
64 |
65 | def create(self, convertToSolid):
66 | """Create a pipe which is a differences of two cilinders: outer cylinder - inner cylinder.
67 |
68 | :param convertToSolid: if true, the resulting part will be solid.
69 | if false, the resulting part will be a cut.
70 | :return resulting part.
71 | """
72 | self.checkDimensions()
73 | # Create outer cylinder.
74 | outer_cylinder = self.document.addObject(
75 | "Part::Cylinder", "OuterCylinder")
76 | outer_cylinder.Radius = self.OD / 2
77 | outer_cylinder.Height = self.H
78 |
79 | # Create inner cylinder. It is a little bit longer than the outer cylider in both ends.
80 | # This should prevent numerical problems when calculating difference
81 | # between the outer and innter cylinder.
82 | inner_cylinder = self.document.addObject(
83 | "Part::Cylinder", "InnerCylinder")
84 | inner_cylinder.Radius = self.OD / 2 - self.Thk
85 | inner_cylinder.Height = self.H * (1 + 2 * RELATIVE_EPSILON)
86 | inner_cylinder.Placement.Base = FreeCAD.Vector(
87 | 0, 0, -self.H * RELATIVE_EPSILON)
88 | pipe = self.document.addObject("Part::Cut", "Pipe")
89 | pipe.Base = outer_cylinder
90 | pipe.Tool = inner_cylinder
91 |
92 | if convertToSolid:
93 | # Before making a solid, recompute documents. Otherwise there will be
94 | # s = Part.Solid(Part.Shell(s))
95 | # : Shape is null
96 | # exception.
97 | self.document.recompute()
98 | # Now convert all parts to solid, and remove intermediate data.
99 | solid = Piping.toSolid(self.document, pipe, "pipe (solid)")
100 | Piping.removePartWithChildren(self.document, pipe)
101 | return solid
102 | return pipe
103 |
104 |
105 | def getDFPipe(obj, DN, OD, thk, H):
106 | """Get pipe features from Dodo or from Flamingo workbench. """
107 | try:
108 | import pFeatures
109 | FreeCAD.Console.PrintMessage("Creating Dodo pipe.")
110 | return pFeatures.Pipe(obj, DN=DN, OD=OD, thk=thk, H=H)
111 | except ModuleNotFoundError:
112 | FreeCAD.Console.PrintMessage("Dodo workbench is not found. I will use Flamingo instead.")
113 | import pipeFeatures
114 | return pipeFeatures.Pipe(obj, DN=DN, OD=OD, thk=thk, H=H)
115 |
116 |
117 | class PipeFromTable:
118 | """Create a part with dimensions from a CSV table."""
119 |
120 | def __init__(self, document, table):
121 | self.document = document
122 | self.table = table
123 |
124 | def create(self, partName, length, outputType):
125 | row = self.table.findPart(partName)
126 | if row is None:
127 | print("Part not found")
128 | return
129 | if outputType == Piping.OUTPUT_TYPE_PARTS or outputType == Piping.OUTPUT_TYPE_SOLID:
130 | pipe = Pipe(self.document)
131 | pipe.OD = parseQuantity(row["OD"])
132 | pipe.Thk = parseQuantity(row["Thk"])
133 | pipe.H = length
134 | part = pipe.create(outputType == Piping.OUTPUT_TYPE_SOLID)
135 | return part
136 | elif outputType == Piping.OUTPUT_TYPE_DODO_OR_FLAMINGO:
137 | # See Code in pipeCmd.makePipe in the Flamingo workbench.
138 | feature = self.document.addObject(
139 | "Part::FeaturePython", "OSE-Pipe")
140 | DN = Piping.GetDnString(row)
141 | OD = parseQuantity(row["OD"])
142 | Thk = parseQuantity(row["Thk"])
143 | part = getDFPipe(feature, DN=DN, OD=OD, thk=Thk, H=length)
144 | feature.PRating = Piping.GetPressureRatingString(row)
145 | # Currently I do not know how to interprite table data as a profile.
146 | feature.Profile = ""
147 | if "PSize" in row.keys():
148 | feature.PSize = row["PSize"]
149 | # Workaround. Add ports before return. Otherwise the positioning is not working.
150 | feature.Ports = [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, length)]
151 | feature.ViewObject.Proxy = 0
152 | return part
153 |
154 |
155 | # Test macros.
156 | def TestPipe():
157 | document = FreeCAD.activeDocument()
158 | pipe = Pipe(document)
159 | pipe.create(False)
160 | document.recompute()
161 |
162 |
163 | def TestTable():
164 | document = FreeCAD.activeDocument()
165 | table = Piping.CsvTable(DIMENSIONS_USED)
166 | table.load(CSV_TABLE_PATH)
167 | pipe = PipeFromTable(document, table)
168 | for i in range(0, len(table.data)):
169 | print("Selecting row %d" % i)
170 | partName = table.getPartName(i)
171 | print("Creating part %s" % partName)
172 | pipe.create(partName, parseQuantity("1m"), False)
173 | document.recompute()
174 |
--------------------------------------------------------------------------------
/OsePiping/FlCorner.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Authors: Ruslan Krenzler, oddtopus.
3 | # Date: 25 March 2018
4 | # Create an outer corner using Flamingo workbench.
5 |
6 | import FreeCAD
7 | import Part
8 | # Parent class from Dodo or Flamingo.
9 | try:
10 | from pFeatures import pypeType
11 | except ModuleNotFoundError:
12 | from pipeFeatures import pypeType
13 | import OsePiping.Corner as CornerMod
14 |
15 |
16 | class Corner(pypeType):
17 | def __init__(self, obj, PSize="", dims=CornerMod.Dimensions()):
18 | """Create an outer corner with the center at (0,0,0) and elbows along x, y and z axis. """
19 | # Run parent __init__ and define common attributes
20 | super(Corner, self).__init__(obj)
21 | obj.PType = "OSE_Corner"
22 | obj.PRating = ""
23 | obj.PSize = PSize
24 | # Define specific attributes and set their values.
25 | obj.addProperty("App::PropertyLength", "G", "Corner",
26 | "Distnace from the center to begin of innerpart of the socket").G = dims.G
27 | obj.addProperty("App::PropertyLength", "H", "Corner",
28 | "Distance between the center and a corner end").H = dims.H
29 | obj.addProperty("App::PropertyLength", "M", "Corner",
30 | "Outside diameter of the corner.").M = dims.M
31 | obj.addProperty("App::PropertyLength", "POD", "Corner",
32 | "Pipe outer diameter.").POD = dims.POD
33 | obj.addProperty("App::PropertyLength", "PThk", "Corner",
34 | "Thickness of the pipe.").PThk = dims.PThk
35 |
36 | # New Dodo-pypeType already contains Ports, use them.
37 | # But if pypeType does does not have Ports, add them.
38 | if "App::PropertyVectorList" not in obj.supportedProperties():
39 | obj.addProperty("App::PropertyVectorList", "Ports", "Corner", "Ports relative positions.")
40 | obj.Ports = self.getPorts(obj)
41 |
42 | obj.addProperty("App::PropertyVectorList", "PortRotationAngles", "Corner",
43 | "Ports rotation angles.").PortRotationAngles = self.getPortRotationAngles(obj)
44 | obj.addProperty("App::PropertyString", "PartNumber",
45 | "Corner", "Part number").PartNumber = ""
46 | # Make Ports read only.
47 | obj.setEditorMode("Ports", 1)
48 | obj.setEditorMode("PortRotationAngles", 1)
49 |
50 | def onChanged(self, obj, prop):
51 | # if you aim to do something when an attribute is changed
52 | # place the code here:
53 | # e.g. -> change PSize according the new alpha, PID and POD
54 |
55 | dim_properties = ["G"] # Dimensions which influence port coordinates.
56 | if prop in dim_properties:
57 | # This function is called within __init__ too. Thus we need to wait until
58 | # we have all the required attributes.
59 | if set(CornerMod.DIMENSIONS_USED).issubset(obj.PropertiesList):
60 | obj.Ports = self.getPorts(obj)
61 | # Wait until PortRotationAngles are defined.
62 | if hasattr(obj, "PortRotationAngles"):
63 | obj.PortRotationAngles = self.getPortRotationAngles(obj)
64 |
65 | @classmethod
66 | def extractDimensions(cls, obj):
67 | dims = CornerMod.Dimensions()
68 | dims.G = obj.G
69 | dims.H = obj.H
70 | dims.M = obj.M
71 | dims.POD = obj.POD
72 | dims.PThk = obj.PThk
73 | return dims
74 |
75 | @classmethod
76 | def createPrimitiveCorner(cls, L, D):
77 | """Create corner consisting of two cylinder along x-,y- and y axis and a ball in the center."""
78 | x_cylinder = Part.makeCylinder(
79 | D / 2, L, FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(1, 0, 0))
80 | y_cylinder = Part.makeCylinder(
81 | D / 2, L, FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 1, 0))
82 | z_cylinder = Part.makeCylinder(
83 | D / 2, L, FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, 1))
84 | sphere = Part.makeSphere(D / 2)
85 | return sphere.fuse([x_cylinder, y_cylinder, z_cylinder])
86 |
87 | @classmethod
88 | def createOuterPart(cls, obj):
89 | dims = cls.extractDimensions(obj)
90 | return cls.createPrimitiveCorner(dims.H, dims.M)
91 |
92 | @classmethod
93 | def createInnerPart(cls, obj):
94 | dims = cls.extractDimensions(obj)
95 | inner = cls.createPrimitiveCorner(dims.H, dims.PID())
96 | inner = Corner.addSockets(dims.POD, dims.H, dims.G, inner)
97 | return inner
98 |
99 | @classmethod
100 | def addSockets(cls, D, H, G, shape):
101 | """Add socket cylinders with diamater D to the ends of the corner shape."""
102 | x_socket = Part.makeCylinder(
103 | D / 2, H - G, FreeCAD.Vector(G, 0, 0), FreeCAD.Vector(1, 0, 0))
104 | y_socket = Part.makeCylinder(
105 | D / 2, H - G, FreeCAD.Vector(0, G, 0), FreeCAD.Vector(0, 1, 0))
106 | z_socket = Part.makeCylinder(
107 | D / 2, H - G, FreeCAD.Vector(0, 0, G), FreeCAD.Vector(0, 0, 1))
108 | return shape.fuse([x_socket, y_socket, z_socket])
109 |
110 | @classmethod
111 | def createShape(cls, obj):
112 | outer = cls.createOuterPart(obj)
113 | inner = cls.createInnerPart(obj)
114 | return outer.cut(inner)
115 |
116 | def execute(self, obj):
117 | # Create the shape of the tee.
118 | shape = Corner.createShape(obj)
119 | obj.Shape = shape
120 | # Recalculate ports.
121 | obj.Ports = self.getPorts(obj)
122 |
123 | def getPorts(self, obj):
124 | """Calculate coordinates of the ports."""
125 | dims = self.extractDimensions(obj)
126 | aux = dims.calculateAuxiliararyPoints()
127 | return [aux["p1"], aux["p2"], aux["p3"]] # x, y, z.
128 |
129 | @classmethod
130 | def getPortRotationAngles(cls, obj):
131 | """Calculate coordinates of the ports rotation and return them as vectorsself.
132 |
133 | x = Yaw
134 | y = Pitch
135 | z = Roll
136 | """
137 | x = FreeCAD.Vector(0, 0, 0)
138 | y = FreeCAD.Vector(90, 0, 0)
139 | z = FreeCAD.Vector(0, -90, 0)
140 | return [x, y, z]
141 |
142 |
143 | class CornerBuilder:
144 | """Create a corner using Dodo/Flamingo."""
145 |
146 | def __init__(self, document):
147 | self.dims = CornerMod.Dimensions()
148 | self.pos = FreeCAD.Vector(0, 0, 0) # Define default initial position.
149 | self.document = document
150 |
151 | def create(self, obj):
152 | """Create a corner.
153 |
154 | Before call it, call
155 | feature = self.document.addObject("Part::FeaturePython","OSE-Corner")
156 | """
157 | corner = Corner(obj, PSize="", dims=self.dims)
158 | obj.ViewObject.Proxy = 0
159 | obj.Placement.Base = self.pos
160 |
161 | return corner
162 |
163 | # Create a test corner.
164 |
165 |
166 | def Test():
167 | document = FreeCAD.activeDocument()
168 | builder = CornerBuilder(document)
169 | feature = document.addObject("Part::FeaturePython", "OSE-Corner")
170 | builder.create(feature)
171 | document.recompute()
172 |
173 | # Test()
174 |
--------------------------------------------------------------------------------
/OsePiping/FlBushing.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Authors: Ruslan Krenzler, oddtopus.
3 | # Date: 25 March 2018
4 | # Create a bushing using Flamingo workbench.
5 |
6 | import FreeCAD
7 | import Part
8 | # Parent class from Dodo or Flamingo.
9 | try:
10 | from pFeatures import pypeType
11 | except ModuleNotFoundError:
12 | from pipeFeatures import pypeType
13 | import OsePiping.Bushing as BushingMod
14 |
15 |
16 | class Bushing(pypeType):
17 | def __init__(self, obj, PSize="", dims=BushingMod.Dimensions()):
18 | """Create a bushing."""
19 | # Run parent __init__ and define common attributes
20 | super(Bushing, self).__init__(obj)
21 | obj.PType = "OSE_Bushing"
22 | obj.PRating = "BushingFittingFromAnyCatalog"
23 | obj.PSize = PSize # What is it for?
24 | # Define specific attributes and set their values.
25 | obj.addProperty("App::PropertyLength", "L", "Bushing",
26 | "Bushing length").L = dims.L
27 | obj.addProperty("App::PropertyLength", "N", "Bushing", "N").N = dims.N
28 | obj.addProperty("App::PropertyLength", "POD", "Bushing",
29 | "Large pipe outer diameter.").POD = dims.POD
30 | obj.addProperty("App::PropertyLength", "POD1", "Bushing",
31 | "Small pipe outer diameter.").POD1 = dims.POD1
32 | obj.addProperty("App::PropertyLength", "PThk1", "Bushing",
33 | "Small pipe thickness.").PThk1 = dims.PThk1
34 |
35 | # New Dodo-pypeType already contains Ports, use them.
36 | # But if pypeType does does not have Ports, add them.
37 | if "App::PropertyVectorList" not in obj.supportedProperties():
38 | obj.addProperty("App::PropertyVectorList", "Ports", "Bushing", "Ports relative positions.")
39 | obj.Ports = self.getPorts(obj)
40 |
41 | obj.addProperty("App::PropertyVectorList", "PortRotationAngles", "Bushing",
42 | "Ports rotation angles.").PortRotationAngles = self.getPortRotationAngles(obj)
43 | obj.addProperty("App::PropertyString", "PartNumber",
44 | "Bushing", "Part number").PartNumber = ""
45 | # Make Ports read only.
46 | obj.setEditorMode("Ports", 1)
47 | obj.setEditorMode("PortRotationAngles", 1)
48 |
49 | def onChanged(self, obj, prop):
50 | # Attributes changed, adjust the rest.
51 | dim_properties = ["L", "N"] # Dimensions which change port locations
52 | if prop in dim_properties:
53 | # This function is called within __init__ too.
54 | # We wait for all dimension.
55 | if set(BushingMod.DIMENSIONS_USED).issubset(obj.PropertiesList):
56 | obj.Ports = self.getPorts(obj)
57 | # We also wait until PortRotationAngles are defined.
58 | # We do not need to call getPortRotationAngles, when the fitting is created,
59 | # because during the createion getPortRotationAngles is called directly.
60 | if hasattr(obj, "PortRotationAngles"):
61 | obj.PortRotationAngles = self.getPortRotationAngles(obj)
62 |
63 | @classmethod
64 | def extractDimensions(cls, obj):
65 | dims = BushingMod.Dimensions()
66 | dims.L = obj.L
67 | dims.N = obj.N
68 | dims.POD = obj.POD
69 | dims.POD1 = obj.POD1
70 | dims.PThk1 = obj.PThk1
71 | return dims
72 |
73 | @classmethod
74 | def createOctaThing(cls, obj):
75 | """Create Octagonal thing at the end of the bushing. I do not know its name."""
76 | dims = cls.extractDimensions(obj)
77 | aux = dims.auxiliararyPoints()
78 | X1 = dims.ThingThicknessA1()
79 | X2 = dims.ThingLengthA2()
80 | # Move the box into the center of the X,Y plane.
81 | center_pos = FreeCAD.Vector(-X2 / 2, -X2 / 2, 0) + aux["p4"]
82 | box1 = Part.makeBox(X2, X2, X1, center_pos)
83 | # rotate a box by 45° around the z.axis.
84 | box2 = Part.makeBox(X2, X2, X1, center_pos)
85 | box2.rotate(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, 1), 45)
86 | return box1.common(box2)
87 |
88 | @classmethod
89 | def createOuterPart(cls, obj):
90 | dims = cls.extractDimensions(obj)
91 | aux = dims.auxiliararyPoints()
92 | outer_cylinder = Part.makeCylinder(dims.POD / 2, dims.L, aux["p1"])
93 | thing = Bushing.createOctaThing(obj)
94 | return outer_cylinder.fuse(thing)
95 |
96 | @classmethod
97 | def createInnerPart(cls, obj):
98 | dims = cls.extractDimensions(obj)
99 | aux = dims.auxiliararyPoints()
100 |
101 | # Remove inner part of the sockets.
102 | inner_cylinder = Part.makeCylinder(dims.PID1() / 2, dims.L, aux["p1"])
103 | inner_socket = Part.makeCylinder(
104 | dims.POD1 / 2, dims.L - dims.N, aux["p3"])
105 |
106 | # Make a cone for a larger socket. There are no dimensions for this con. Therefore
107 | # use simbolically a Radius such that the wall at the lower end is twice as thick
108 | # as in the upper end of socket.
109 | r1 = dims.POD / 2 - dims.ThicknessA3()
110 | r2 = dims.PID1() / 2
111 | hcone = dims.ConeLengthA4()
112 | socket_cone = Part.makeCone(r1, r2, hcone, aux["p1"])
113 |
114 | return inner_cylinder.fuse([inner_socket, socket_cone])
115 |
116 | @classmethod
117 | def createShape(cls, obj):
118 | outer = cls.createOuterPart(obj)
119 | inner = cls.createInnerPart(obj)
120 | return outer.cut(inner)
121 |
122 | def execute(self, obj):
123 | # Create the shape of the bushing.
124 | shape = self.createShape(obj)
125 | obj.Shape = shape
126 | # Recalculate ports.
127 | obj.Ports = self.getPorts(obj)
128 |
129 | def getPorts(self, obj):
130 | """Calculate coordinates of the ports."""
131 | dims = self.extractDimensions(obj)
132 | aux = dims.auxiliararyPoints()
133 | # For the bottom port use p3 too. Because there is no a1 dimension in my specification.
134 | return[aux["p3"], aux["p3"]]
135 |
136 | @classmethod
137 | def getPortRotationAngles(cls, obj):
138 | """Calculate coordinates of the ports rotation and return them as vectorsself.
139 |
140 | x = Yaw
141 | y = Pitch
142 | z = Roll
143 | """
144 | outer = FreeCAD.Vector(0, 90, 0)
145 | inner = FreeCAD.Vector(0, -90, 0)
146 |
147 | return [outer, inner]
148 |
149 |
150 | class BushingBuilder:
151 | """Create a bushing using Dodo/Flamingo."""
152 |
153 | def __init__(self, document):
154 | self.dims = BushingMod.Dimensions()
155 | self.pos = FreeCAD.Vector(0, 0, 0) # Define default initial position.
156 | self.document = document
157 |
158 | def create(self, obj):
159 | """Create a bushing.
160 |
161 | Before call it, call
162 | feature = self.document.addObject("Part::FeaturePython","OSE-Bushing")
163 | """
164 | bushing = Bushing(obj, PSize="", dims=self.dims)
165 | obj.ViewObject.Proxy = 0
166 | obj.Placement.Base = self.pos
167 |
168 | return bushing
169 |
170 | # Create a test bushing.
171 |
172 |
173 | def Test():
174 | document = FreeCAD.activeDocument()
175 | builder = BushingBuilder(document)
176 | feature = document.addObject("Part::FeaturePython", "OSE-Bushing")
177 | builder.create(feature)
178 | document.recompute()
179 |
180 | # Test()
181 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
167 |
--------------------------------------------------------------------------------
/Resources/icons/CreatePipe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
222 |
--------------------------------------------------------------------------------
/OsePiping/FlCross.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Authors: oddtopus, Ruslan Krenzler
3 | # Date: 8. April 2018
4 |
5 | # Create a cross-fitting using Flamingo workbench.
6 |
7 | import FreeCAD
8 | import Part
9 | # Parent class from Dodo or Flamingo.
10 | try:
11 | from pFeatures import pypeType
12 | except ModuleNotFoundError:
13 | from pipeFeatures import pypeType
14 | import OsePiping.Cross as CrossMod
15 |
16 |
17 | class Cross(pypeType):
18 | def __init__(self, obj, PSize="90degBend20x10", dims=CrossMod.Dimensions()):
19 | # run parent __init__ and define common attributes
20 | super(Cross, self).__init__(obj)
21 | obj.PType = "OSE_Cross"
22 | obj.PRating = "CrossFittingFromAnyCatalog"
23 | obj.PSize = PSize # Pipe size
24 | # define specific attributes
25 | # Properties
26 | obj.addProperty("App::PropertyLength", "G", "Cross",
27 | "G dimension of the cross.").G = dims.G
28 | obj.addProperty("App::PropertyLength", "G1", "Cross",
29 | "G1 dimension of the cross.").G1 = dims.G1
30 | obj.addProperty("App::PropertyLength", "H", "Cross",
31 | "Distance from the center to the left end.").H = dims.H
32 | obj.addProperty("App::PropertyLength", "H1", "Cross",
33 | "Dimension from the center to the bottom end.").H1 = dims.H1
34 | obj.addProperty("App::PropertyLength", "L", "Cross",
35 | "Horizontal length of the cross.").L = dims.L
36 | obj.addProperty("App::PropertyLength", "L1", "Cross",
37 | "Vertical length of the cross.").L1 = dims.L1
38 | obj.addProperty("App::PropertyLength", "M", "Cross",
39 | "Outer diameter of the horizonal part.").M = dims.M
40 | obj.addProperty("App::PropertyLength", "M1", "Cross",
41 | "Outer diameter of the vertical part.").M1 = dims.M1
42 | obj.addProperty("App::PropertyLength", "POD", "Cross",
43 | "Pipe outer diameter of the horizontal part.").POD = dims.POD
44 | obj.addProperty("App::PropertyLength", "POD1", "Cross",
45 | "Pipe outer Diameter of the vertical part.").POD1 = dims.POD1
46 | obj.addProperty("App::PropertyLength", "PThk", "Cross",
47 | "Pipe wall thickness of the horizonal part").PThk = dims.PThk
48 | obj.addProperty("App::PropertyLength", "PThk1", "Cross",
49 | "Pipe wall thickness of the vertical part").PThk1 = dims.PThk1
50 | obj.addProperty("App::PropertyString", "PartNumber",
51 | "Cross", "Part number").PartNumber = ""
52 |
53 | # New Dodo-pypeType already contains Ports, use them.
54 | # But if pypeType does does not have Ports, add them.
55 | if "App::PropertyVectorList" not in obj.supportedProperties():
56 | obj.addProperty("App::PropertyVectorList", "Ports", "Cross", "Ports relative positions.")
57 | obj.Ports = self.getPorts(obj)
58 |
59 | obj.addProperty("App::PropertyVectorList", "PortRotationAngles", "Cross",
60 | "Ports rotation angles.").PortRotationAngles = self.getPortRotationAngles(obj)
61 | # Make Ports read only.
62 | obj.setEditorMode("Ports", 1)
63 | obj.setEditorMode("PortRotationAngles", 1)
64 |
65 | def onChanged(self, obj, prop):
66 | # if you aim to do something when an attribute is changed
67 | # place the code here:
68 | # e.g. -> change PSize according the new alpha, PID and POD
69 |
70 | # Properties which can change port locations
71 | dim_properties = ["G", "G1"]
72 |
73 | if prop in dim_properties:
74 | # This function is called within __init__ too. Thus we need to wait until
75 | # we have all dimensions attributes.
76 | if set(CrossMod.DIMENSIONS_USED).issubset(obj.PropertiesList):
77 | obj.Ports = self.getPorts(obj)
78 | # Wait until PortRotationAngles are defined.
79 | if hasattr(obj, "PortRotationAngles"):
80 | obj.PortRotationAngles = self.getPortRotationAngles(obj)
81 |
82 | @classmethod
83 | def extractDimensions(cls, obj):
84 | dims = CrossMod.Dimensions()
85 | dims.G = obj.G
86 | dims.G1 = obj.G1
87 | dims.H = obj.H
88 | dims.H1 = obj.H1
89 | dims.L = obj.L
90 | dims.L1 = obj.L1
91 | dims.M = obj.M
92 | dims.M1 = obj.M1
93 | dims.POD = obj.POD
94 | dims.POD1 = obj.POD1
95 | dims.PThk = obj.PThk
96 | dims.PThk1 = obj.PThk1
97 | return dims
98 |
99 | @classmethod
100 | def createOuterPart(cls, obj):
101 | dims = cls.extractDimensions(obj)
102 | aux = dims.calculateAuxiliararyPoints()
103 | hr = dims.M / 2
104 | hor_cylinder = Part.makeCylinder(
105 | hr, dims.L, aux["p1"], FreeCAD.Vector(1, 0, 0))
106 | vr = dims.M1 / 2
107 | vert_cylinder = Part.makeCylinder(vr, dims.L1, aux["p4"])
108 | outer = hor_cylinder.fuse(vert_cylinder)
109 | return outer
110 |
111 | @classmethod
112 | def createInnerPart(cls, obj):
113 | dims = cls.extractDimensions(obj)
114 | aux = dims.calculateAuxiliararyPoints()
115 | p1 = aux["p1"]
116 | p3 = aux["p3"]
117 | p4 = aux["p4"]
118 | p6 = aux["p6"]
119 | pid = dims.PID()
120 | pid1 = dims.PID1()
121 | pod = dims.POD
122 | pod1 = dims.POD1
123 | hr = pid / 2
124 | hor_cylinder = Part.makeCylinder(
125 | hr, dims.L, p1, FreeCAD.Vector(1, 0, 0))
126 | vr = pid1 / 2
127 | vert_cylinder = Part.makeCylinder(vr, dims.L1, p4)
128 |
129 | # Create sockets.
130 | socket_left = Part.makeCylinder(
131 | pod / 2, dims.socketDepthLeft(), p1, FreeCAD.Vector(1, 0, 0))
132 | socket_right = Part.makeCylinder(
133 | pod / 2, dims.socketDepthRight(), p3, FreeCAD.Vector(1, 0, 0))
134 | socket_bottom = Part.makeCylinder(
135 | pod1 / 2, dims.socketDepthBottom(), p4)
136 | socket_top = Part.makeCylinder(pod1 / 2, dims.socketDepthTop(), p6)
137 |
138 | # Combine all cylinders.
139 | inner = hor_cylinder.fuse([vert_cylinder, socket_left,
140 | socket_right, socket_bottom, socket_top])
141 | return inner
142 |
143 | @classmethod
144 | def createShape(cls, obj):
145 | outer = cls.createOuterPart(obj)
146 | inner = cls.createInnerPart(obj)
147 | return outer.cut(inner)
148 | # return outer
149 |
150 | def execute(self, obj):
151 | # Create the shape of the tee.
152 | shape = self.createShape(obj)
153 | obj.Shape = shape
154 | # Recalculate ports.
155 | obj.Ports = self.getPorts(obj)
156 |
157 | def getPorts(self, obj):
158 | dims = Cross.extractDimensions(obj)
159 | aux = dims.calculateAuxiliararyPoints()
160 | return [aux["p2"], aux["p3"], aux["p5"], aux["p6"]]
161 |
162 | @classmethod
163 | def getPortRotationAngles(cls, obj):
164 | """Calculate coordinates of the ports rotation and return them as vectorsself.
165 |
166 | x = Yaw
167 | y = Pitch
168 | z = Roll
169 | """
170 | left = FreeCAD.Vector(180, 0, 180)
171 | right = FreeCAD.Vector(0, 0, 0)
172 | bottom = FreeCAD.Vector(0, 90, 0)
173 | top = FreeCAD.Vector(0, -90, 0)
174 | return [left, right, bottom, top]
175 |
176 |
177 | class CrossBuilder:
178 | """Create a cross using Dodo/Flamingo."""
179 |
180 | def __init__(self, document):
181 | self.dims = CrossMod.Dimensions()
182 | self.pos = FreeCAD.Vector(0, 0, 0) # Define default initial position.
183 | self.document = document
184 |
185 | def create(self, obj):
186 | """Create a cross."""
187 | cross = Cross(obj, PSize="", dims=self.dims)
188 | obj.ViewObject.Proxy = 0
189 | obj.Placement.Base = self.pos
190 | # rot=FreeCAD.Rotation(FreeCAD.Vector(0,0,1), self.Z)
191 | # obj.Placement.Rotation=rot.multiply(obj.Placement.Rotation)
192 | # feature.ViewObject.Transparency=70
193 | return cross
194 |
195 | # Test builder.
196 |
197 |
198 | def TestPart():
199 | document = FreeCAD.activeDocument()
200 | builder = CrossBuilder(document)
201 | feature = document.addObject("Part::FeaturePython", "OSE-Cross")
202 | builder.create(feature)
203 | document.recompute()
204 |
205 | # TestPart()
206 |
--------------------------------------------------------------------------------
/OsePiping/Corner.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Author: Ruslan Krenzler.
3 | # Date: 09 February 2018
4 | # Create a corner-fitting.
5 |
6 | import os.path
7 | import FreeCAD
8 | import OsePipingBase
9 | import OsePiping.Piping as Piping
10 |
11 | parseQuantity = FreeCAD.Units.parseQuantity
12 |
13 | # This is the path to the dimensions table.
14 | CSV_TABLE_PATH = os.path.join(OsePipingBase.TABLE_PATH, "corner.csv")
15 | # It must contain unique values in the column "PartNumber" and also, dimensions listened below.
16 | DIMENSIONS_USED = ["G", "H", "M", "POD", "PThk"]
17 |
18 |
19 | class Dimensions:
20 | def __init__(self):
21 | self.G = parseQuantity("2 cm")
22 | self.H = parseQuantity("3 cm")
23 | self.M = parseQuantity("3 cm")
24 | self.POD = parseQuantity("2 cm")
25 | self.PThk = parseQuantity("0.5 cm")
26 |
27 | def isValid(self):
28 | errorMsg = ""
29 | if not (self.POD > 0):
30 | errorMsg = "Pipe outer diameter POD {} must be positive.".format(
31 | self.POD)
32 | elif not (self.PThk <= self.POD / 2.0):
33 | errorMsg = "Pipe thickness PThk {} is too large: larger than POD/2 {}.".foramt(
34 | self.PThk, self.POD / 2.0)
35 | elif not (self.M > self.POD):
36 | errorMsg = "Outer diameter M {} must be larger than outer pipe diameter POD {}".format(
37 | self.M, self.POD)
38 | elif not (self.G > 0):
39 | errorMsg = "Length G {} be positive".format(self.G)
40 | elif not (self.H > self.G):
41 | errorMsg = "Length H {} must be larger than length G {}".format(
42 | self.H, self.G)
43 | if not (self.G > self.PID() / 2.0):
44 | errorMsg = "Length G {} must be larger than inner pipe radius PID/2={}.".format(
45 | self.G, self.PID() / 2.0)
46 |
47 | return (len(errorMsg) == 0, errorMsg)
48 |
49 | def PID(self):
50 | return self.POD - 2 * self.PThk
51 |
52 | def calculateAuxiliararyPoints(self):
53 | """Calculate auxiliarary points which are used to build a corner from cylinders.
54 |
55 | See documentation picture corner-cacluations.png.
56 | """
57 | result = {}
58 | result["p1"] = FreeCAD.Vector(self.G, 0, 0)
59 | result["p2"] = FreeCAD.Vector(0, self.G, 0)
60 | result["p3"] = FreeCAD.Vector(0, 0, self.G)
61 | return result
62 |
63 |
64 | class Corner:
65 | def __init__(self, document):
66 | self.document = document
67 | # Set default values.
68 | self.dims = Dimensions()
69 |
70 | def checkDimensions(self):
71 | valid, msg = self.dims.isValid()
72 | if not valid:
73 | raise Piping.UnplausibleDimensions(msg)
74 |
75 | def createPrimitiveCorner(self, L, D):
76 | """Create corner consisting of two cylinder along x-,y- and y axis and a ball in the center."""
77 | x_cylinder = self.document.addObject("Part::Cylinder", "XCynlider")
78 | x_cylinder.Radius = D / 2
79 | x_cylinder.Height = L
80 | x_cylinder.Placement = FreeCAD.Placement(FreeCAD.Vector(
81 | 0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), 90), FreeCAD.Vector(0, 0, 0))
82 | y_cylinder = self.document.addObject("Part::Cylinder", "YCynlider")
83 | y_cylinder.Radius = D / 2
84 | y_cylinder.Height = L
85 | y_cylinder.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(
86 | FreeCAD.Vector(1, 0, 0), -90), FreeCAD.Vector(0, 0, 0))
87 | z_cylinder = self.document.addObject("Part::Cylinder", "ZCynlider")
88 | z_cylinder.Radius = D / 2
89 | z_cylinder.Height = L
90 | sphere = self.document.addObject("Part::Sphere", "Sphere")
91 | sphere.Radius = D / 2
92 | fusion = self.document.addObject("Part::MultiFuse", "Fusion")
93 | fusion.Shapes = [x_cylinder, y_cylinder, z_cylinder, sphere]
94 | return fusion
95 |
96 | def addSockets(self, fusion):
97 | """Add socket cylinders to the fusion."""
98 | x_socket = self.document.addObject("Part::Cylinder", "XSocket")
99 | x_socket.Radius = self.dims.POD / 2
100 | x_socket.Height = self.dims.H - self.dims.G
101 | x_socket.Placement = FreeCAD.Placement(FreeCAD.Vector(self.dims.G, 0, 0), FreeCAD.Rotation(
102 | FreeCAD.Vector(0, 1, 0), 90), FreeCAD.Vector(0, 0, 0))
103 | y_socket = self.document.addObject("Part::Cylinder", "YSocket")
104 | y_socket.Radius = self.dims.POD / 2
105 | y_socket.Height = self.dims.H - self.dims.G
106 | y_socket.Placement = FreeCAD.Placement(FreeCAD.Vector(0, self.dims.G, 0), FreeCAD.Rotation(
107 | FreeCAD.Vector(1, 0, 0), -90), FreeCAD.Vector(0, 0, 0))
108 | z_socket = self.document.addObject("Part::Cylinder", "ZSocket")
109 | z_socket.Radius = self.dims.POD / 2
110 | z_socket.Height = self.dims.H - self.dims.G
111 | z_socket.Placement.Base = FreeCAD.Vector(0, 0, self.dims.G)
112 | # fusion.Shapes.append does not work.
113 | fusion.Shapes = fusion.Shapes + [x_socket, y_socket, z_socket]
114 | return fusion
115 |
116 | def createOuterPart(self):
117 | return self.createPrimitiveCorner(self.dims.H, self.dims.M)
118 |
119 | def createInnerPart(self):
120 | inner = self.createPrimitiveCorner(self.dims.H, self.dims.PID())
121 | inner = self.addSockets(inner)
122 | return inner
123 |
124 | def create(self, convertToSolid):
125 | self.checkDimensions()
126 | outer = self.createOuterPart()
127 | inner = self.createInnerPart()
128 | # Remove inner part of the sockets.
129 | corner = self.document.addObject("Part::Cut", "Cut")
130 | corner.Base = outer
131 | corner.Tool = inner
132 |
133 | if convertToSolid:
134 | # Before making a solid, recompute documents. Otherwise there will be
135 | # s = Part.Solid(Part.Shell(s))
136 | # : Shape is null
137 | # exception.
138 | self.document.recompute()
139 | # Now convert all parts to solid, and remove intermediate data.
140 | solid = Piping.toSolid(self.document, corner, "corner (solid)")
141 | Piping.removePartWithChildren(self.document, corner)
142 | return solid
143 | return corner
144 |
145 |
146 | class CornerFromTable:
147 | """Create a part with dimensions from a CSV table."""
148 |
149 | def __init__(self, document, table):
150 | self.document = document
151 | self.table = table
152 |
153 | @classmethod
154 | def getPThk(cls, row):
155 | """For compatibility results, if there is no "PThk" dimension, calculate it from "PID" and "POD"."""
156 | if "PThk" not in row.keys():
157 | return (parseQuantity(row["POD"]) - parseQuantity(row["PID"])) / 2.0
158 | else:
159 | return parseQuantity(row["PThk"])
160 |
161 | @classmethod
162 | def getPSize(cls, row):
163 | if "PSize" in row.keys():
164 | return row["PSize"]
165 | else:
166 | return ""
167 |
168 | def create(self, partNumber, outputType):
169 | row = self.table.findPart(partNumber)
170 | if row is None:
171 | print("Part not found")
172 | return
173 |
174 | dims = Dimensions()
175 | dims.G = parseQuantity(row["G"])
176 | dims.H = parseQuantity(row["H"])
177 | dims.M = parseQuantity(row["M"])
178 | dims.POD = parseQuantity(row["POD"])
179 | dims.PThk = self.getPThk(row)
180 |
181 | if outputType == Piping.OUTPUT_TYPE_PARTS or outputType == Piping.OUTPUT_TYPE_SOLID:
182 | corner = Corner(self.document)
183 | corner.dims = dims
184 | part = corner.create(outputType == Piping.OUTPUT_TYPE_SOLID)
185 | part.Label = "OSE-Corner"
186 | return part
187 |
188 | elif outputType == Piping.OUTPUT_TYPE_DODO_OR_FLAMINGO:
189 | feature = self.document.addObject(
190 | "Part::FeaturePython", "OSE-Corner")
191 | import OsePiping.FlCorner as FlCorner
192 | builder = FlCorner.CornerBuilder(self.document)
193 | builder.dims = dims
194 | part = builder.create(feature)
195 | feature.PRating = Piping.GetPressureRatingString(row)
196 | feature.PSize = self.getPSize(row)
197 | feature.ViewObject.Proxy = 0
198 | feature.PartNumber = partNumber
199 | return part
200 |
201 |
202 | # Test macros.
203 | def TestCorner():
204 | document = FreeCAD.activeDocument()
205 | corner = Corner(document)
206 | corner.create(True)
207 | document.recompute()
208 |
209 |
210 | def TestTable():
211 | document = FreeCAD.activeDocument()
212 | table = Piping.CsvTable(DIMENSIONS_USED)
213 | table.load(CSV_TABLE_PATH)
214 | builder = CornerFromTable(document, table)
215 | for i in range(0, len(table.data)):
216 | print("Selecting row %d" % i)
217 | partNumber = table.getPartKey(i)
218 | print("Creating part %s" % partNumber)
219 | builder.create(partNumber, Piping.OUTPUT_TYPE_SOLID)
220 | document.recompute()
221 |
222 |
223 | # TestCorner()
224 | # TestTable()
225 |
--------------------------------------------------------------------------------
/OsePiping/FlSweepElbow.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Authors: Ruslan Krenzler, oddtopus
3 | # Date: 30. March 2018
4 | # Create a sweep-elbow-fitting using flamingo workbench
5 |
6 | import FreeCAD
7 | import Part
8 | # Parent class from Dodo or Flamingo.
9 | try:
10 | from pFeatures import pypeType
11 | except ModuleNotFoundError:
12 | from pipeFeatures import pypeType
13 | import OsePiping.SweepElbow as SweepElbowMod
14 |
15 |
16 | # The value RELATIVE_EPSILON is used to slightly change the size of parts
17 | # to prevent problems with boolean operations.
18 | # Keep this value very small.
19 | # For example, the outer bent part of the elbow dissaperas when it has
20 | # the same radius as the cylinder at the ends.
21 | RELATIVE_EPSILON = 0.000001
22 |
23 |
24 | class SweepElbow(pypeType):
25 | def __init__(self, obj, PSize="", dims=SweepElbowMod.Dimensions()):
26 | """Create a sweep elbow with the center at (0,0,0) sockets along the z and y axis."""
27 | # Run parent __init__ and define common attributes.
28 | super(SweepElbow, self).__init__(obj)
29 | obj.PType = "OSE_SweepElbow"
30 | obj.PRating = ""
31 | obj.PSize = PSize
32 | # Define specific attributes and set their values.
33 | obj.addProperty("App::PropertyAngle", "BendAngle",
34 | "SweepElbow", "Bend Angle.").BendAngle = dims.BendAngle
35 | obj.addProperty("App::PropertyLength", "J", "SweepElbow",
36 | "Distnace from the center to begin of innerpart of the socket").J = dims.J
37 | obj.addProperty("App::PropertyLength", "H", "SweepElbow",
38 | "Distance between the center and a elbow end").H = dims.H
39 | obj.addProperty("App::PropertyLength", "M", "SweepElbow",
40 | "Outer diameter of the elbow.").M = dims.M
41 | obj.addProperty("App::PropertyLength", "POD", "SweepElbow",
42 | "Pipe outer diameter.").POD = dims.POD
43 | obj.addProperty("App::PropertyLength", "PThk",
44 | "SweepElbow", "Pipe wall thickness").PThk = dims.PThk
45 |
46 | # New Dodo-pypeType already contains Ports, use them.
47 | # But if pypeType does does not have Ports, add them.
48 | if "App::PropertyVectorList" not in obj.supportedProperties():
49 | obj.addProperty("App::PropertyVectorList", "Ports", "SweepElbow", "Ports relative positions.")
50 | obj.Ports = self.getPorts(obj)
51 |
52 | obj.addProperty("App::PropertyVectorList", "PortRotationAngles", "SweepElbow",
53 | "Ports rotation angles.").PortRotationAngles = self.getPortRotationAngles(obj)
54 | obj.addProperty("App::PropertyString", "PartNumber",
55 | "SweepElbow", "Part number").PartNumber = ""
56 |
57 | # Make Ports read only.
58 | obj.setEditorMode("Ports", 1)
59 | obj.setEditorMode("PortRotationAngles", 1)
60 |
61 | def onChanged(self, obj, prop):
62 | # if you aim to do something when an attribute is changed
63 | # place the code here:
64 | # e.g. -> change PSize according the new alpha, PID and POD
65 |
66 | # Dimensions which can change port positions.
67 | dim_properties = ["BendAngle", "J"]
68 | if prop in dim_properties:
69 | # This function is called within __init__ too.
70 | # We wait for all dimension.
71 | if set(SweepElbowMod.DIMENSIONS_USED).issubset(obj.PropertiesList):
72 | obj.Ports = self.getPorts(obj)
73 | # Wait until PortRotationAngles are defined.
74 | if hasattr(obj, "PortRotationAngles"):
75 | obj.PortRotationAngles = self.getPortRotationAngles(obj)
76 |
77 | @staticmethod
78 | def extractDimensions(obj):
79 | dims = SweepElbowMod.Dimensions()
80 | dims.BendAngle = obj.BendAngle
81 | dims.H = obj.H
82 | dims.J = obj.J
83 | dims.M = obj.M
84 | dims.POD = obj.POD
85 | dims.PThk = obj.PThk
86 | return dims
87 |
88 | @staticmethod
89 | def createBentCylinder(obj, rCirc):
90 | """Create a cylinder of radius rCirc in x-y plane which is bent in the middle.
91 |
92 | :param group: Group where to add created objects.
93 | :param rCirc: Radius of the cylinder.
94 |
95 | See documentation picture sweep-elbow-cacluations.png.
96 | """
97 | # Convert alpha to degree value
98 | dims = SweepElbow.extractDimensions(obj)
99 |
100 | aux = dims.calculateAuxiliararyPoints()
101 |
102 | alpha = float(dims.BendAngle.getValueAs("deg"))
103 | rBend = (aux["p3"] - aux["p5"]).Length
104 |
105 | # Put a base on the streight part.
106 | base = Part.makeCircle(rCirc, aux["p5"], aux["p5"])
107 |
108 | # Add trajectory
109 | trajectory = Part.makeCircle(
110 | rBend, aux["p3"], FreeCAD.Vector(0, 0, 1), 225 - alpha / 2, 225 + alpha / 2)
111 | # Show trajectory for debugging.
112 | # W = W1.fuse([trajectory.Edges])
113 | # Part.Show(W)
114 | # Add a cap (circle, at the other end of the bent cylinder).
115 | cap = Part.makeCircle(rCirc, aux["p6"], aux["p6"])
116 | # Sweep the circle along the trajectory.
117 | sweep = Part.makeSweepSurface(trajectory, base)
118 | # The sweep is only a 2D service consisting of walls only.
119 | # Add circles on both ends of this wall.
120 | end1 = Part.Face(Part.Wire(base))
121 | # Create other end.
122 | end2 = Part.Face(Part.Wire(cap))
123 | solid = Part.Solid(Part.Shell([end1, sweep, end2]))
124 | return solid
125 |
126 | @staticmethod
127 | def createOuterPart(obj):
128 | dims = SweepElbow.extractDimensions(obj)
129 | aux = dims.calculateAuxiliararyPoints()
130 | # Make the outer part slightly larger. Otherwise it can be shown incorrectly after
131 | # the subtraction of the inner part.
132 | r = ((dims.PID() / 2 + dims.fitThk()) * (1 + RELATIVE_EPSILON))
133 | bentPart = SweepElbow.createBentCylinder(obj, r)
134 | # Create socket along the z axis.
135 | h = float(dims.H) - aux["p2"].Length
136 | r = dims.M / 2
137 | socket1 = Part.makeCylinder(r, h, aux["p2"], aux["p2"])
138 | # Create socket along the bent part.
139 | socket2 = Part.makeCylinder(r, h, aux["p4"], aux["p4"])
140 |
141 | outer = bentPart.fuse([socket1, socket2])
142 | return outer
143 |
144 | @staticmethod
145 | def createInnerPart(obj):
146 | dims = SweepElbow.extractDimensions(obj)
147 | aux = dims.calculateAuxiliararyPoints()
148 |
149 | r = dims.POD / 2 - dims.PThk
150 |
151 | bentPart = SweepElbow.createBentCylinder(
152 | obj, r * (1 + RELATIVE_EPSILON))
153 |
154 | rSocket = dims.POD / 2
155 | # The socket length is actually dims.H - dims.J. But we do it longer
156 | # to prevent problems with bulean operations
157 | hSocket = dims.H
158 | socket1 = Part.makeCylinder(rSocket, hSocket, aux["p5"], aux["p5"])
159 | socket2 = Part.makeCylinder(rSocket, hSocket, aux["p6"], aux["p6"])
160 |
161 | inner = bentPart.fuse([socket1, socket2])
162 | return inner
163 |
164 | @staticmethod
165 | def createShape(obj):
166 | outer = SweepElbow.createOuterPart(obj)
167 | inner = SweepElbow.createInnerPart(obj)
168 | return outer.cut(inner)
169 |
170 | def execute(self, obj):
171 | # Create the shape of the tee.
172 | shape = SweepElbow.createShape(obj)
173 | obj.Shape = shape
174 | # Recalculate ports.
175 | obj.Ports = self.getPorts(obj)
176 |
177 | def getPorts(self, obj):
178 | dims = SweepElbow.extractDimensions(obj)
179 | aux = dims.calculateAuxiliararyPoints()
180 | # FreeCAD.Console.PrintMessage("Ports are %s and %s"%(aux["p5"], aux["p6"]))
181 | return [aux["p5"], aux["p6"]]
182 |
183 | def getPortRotationAngles(self, obj):
184 | """Calculate coordinates of the ports rotation and return them as vectorsself.
185 |
186 | x = Yaw
187 | y = Pitch
188 | z = Roll
189 | """
190 | dims = SweepElbow.extractDimensions(obj)
191 | half = dims.BendAngle / 2
192 | # -45° and 135° are rotation of 0° elbow. They acts as a refence for a bent elbow.
193 | end0 = FreeCAD.Vector(-45 + half.Value, 0, 0)
194 | end1 = FreeCAD.Vector(135 - half.Value, 0, 0)
195 | return [end0, end1]
196 |
197 |
198 | class SweepElbowBuilder:
199 | """Create a sweep elbow using Dodo/flamingo."""
200 |
201 | def __init__(self, document):
202 | self.dims = SweepElbowMod.Dimensions()
203 | self.pos = FreeCAD.Vector(0, 0, 0) # Define default initial position.
204 | self.document = document
205 |
206 | def create(self, obj):
207 | """Create a sweep elbow.
208 |
209 | Before call it, call
210 | feature = self.document.addObject("Part::FeaturePython","OSE-SweepElbow")
211 | """
212 | elbow = SweepElbow(obj, PSize="", dims=self.dims)
213 | obj.ViewObject.Proxy = 0
214 | obj.Placement.Base = self.pos
215 | return elbow
216 |
217 |
218 | # Test builder.
219 | def TestSweepElbow():
220 | document = FreeCAD.activeDocument()
221 | builder = SweepElbowBuilder(document)
222 | feature = document.addObject("Part::FeaturePython", "OSE-SweepElbow")
223 | builder.create(feature)
224 | document.recompute()
225 |
226 | # TestSweepElbow()
227 |
--------------------------------------------------------------------------------
/OsePiping/FlCoupling.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Authors: Ruslan Krenzler, oddtopus.
3 | # Date: 24 March 2018
4 | # Create a coupling using Flamingo workbench.
5 |
6 | import FreeCAD
7 | import Part
8 | # Parent class from Dodo or Flamingo.
9 | try:
10 | from pFeatures import pypeType
11 | except ModuleNotFoundError:
12 | from pipeFeatures import pypeType
13 | import OsePiping.Coupling as CouplingMod
14 |
15 |
16 | class Coupling(pypeType):
17 |
18 | def __init__(self, obj, PSize="", dims=CouplingMod.Dimensions()):
19 | """Create a coupling."""
20 | # Run parent __init__ and define common attributes
21 | super(Coupling, self).__init__(obj)
22 | obj.PType = "OSE_Coupling"
23 | obj.PRating = "CouplingFittingFromAnyCatalog"
24 | obj.PSize = PSize # Pipe size
25 | # Define specific attributes and set their values.
26 | # TODO: Check socket enumerations.
27 | obj.addProperty("App::PropertyLength", "L", "Coupling",
28 | "Length of the coupling").L = dims.L
29 | obj.addProperty("App::PropertyLength", "M", "Coupling",
30 | "Coupling outside diameter.").M = dims.M
31 | obj.addProperty("App::PropertyLength", "M1", "Coupling",
32 | "Coupling outside diameter of the thin end.").M1 = dims.M1
33 | obj.addProperty("App::PropertyLength", "N", "Coupling",
34 | "Length of the middle part of the coupling.").N = dims.N
35 | obj.addProperty("App::PropertyLength", "POD", "Coupling",
36 | "Pipe outer diameter at the socket 1.").POD = dims.POD
37 | obj.addProperty("App::PropertyLength", "POD1", "Coupling",
38 | "Pipe outer diameter at the socket 2.").POD1 = dims.POD1
39 | obj.addProperty("App::PropertyLength", "PThk", "Coupling",
40 | "Thickness of the pipe at the socket 1.").PThk = dims.PThk
41 | obj.addProperty("App::PropertyLength", "PThk1", "Coupling",
42 | "Thickness of the pipe at the socket 2.").PThk1 = dims.PThk1
43 |
44 | # New Dodo-pypeType already contains Ports, use them.
45 | # But if pypeType does does not have Ports, add them.
46 | if "App::PropertyVectorList" not in obj.supportedProperties():
47 | obj.addProperty("App::PropertyVectorList", "Ports", "Coupling", "Ports relative positions.")
48 | obj.Ports = self.getPorts(obj)
49 |
50 | obj.addProperty("App::PropertyVectorList", "PortRotationAngles", "Coupling",
51 | "Ports rotation angles.").PortRotationAngles = self.getPortRotationAngles(obj)
52 | obj.addProperty("App::PropertyString", "PartNumber",
53 | "Coupling", "Part number").PartNumber = ""
54 | # Make Ports read only.
55 | obj.setEditorMode("Ports", 1)
56 | obj.setEditorMode("PortRotationAngles", 1)
57 |
58 | def onChanged(self, obj, prop):
59 | # if you aim to do something when an attribute is changed
60 | # place the code here:
61 | # e.g. -> change PSize according the new alpha, PID and POD
62 |
63 | dim_properties = ["L", "M", "M1", "N"]
64 |
65 | if prop in dim_properties:
66 | # This function is called within __init__ too.
67 | # We wait for all dimension.
68 | if set(CouplingMod.DIMENSIONS_USED).issubset(obj.PropertiesList):
69 | obj.Ports = self.getPorts(obj)
70 | # Wait until PortRotationAngles are defined.
71 | if hasattr(obj, "PortRotationAngles"):
72 | obj.PortRotationAngles = self.getPortRotationAngles(obj)
73 |
74 | @classmethod
75 | def extractDimensions(cls, obj):
76 | dims = CouplingMod.Dimensions()
77 | dims.L = obj.L
78 | dims.M = obj.M
79 | dims.M1 = obj.M1
80 | dims.N = obj.N
81 | dims.POD = obj.POD
82 | dims.POD1 = obj.POD1
83 | dims.PThk = obj.PThk
84 | dims.PThk1 = obj.PThk1
85 | return dims
86 |
87 | @classmethod
88 | def createOuterPart(cls, obj):
89 | dims = cls.extractDimensions(obj)
90 |
91 | if dims.M == dims.M1:
92 | return cls.createOuterPartEqual(obj)
93 | else:
94 | return cls.createOuterPartReduced(obj)
95 |
96 | @classmethod
97 | def createOuterPartEqual(cls, obj):
98 | """Create the outer part is a simple cylinder. This is when M and M1 are the equal."""
99 | dims = cls.extractDimensions(obj)
100 | aux = dims.calculateAuxiliararyPoints()
101 | # Create complete outer cylinder.
102 | radius = dims.M / 2.0
103 | height = dims.L
104 | outer = Part.makeCylinder(radius, height, aux["p1"])
105 | return outer
106 |
107 | @classmethod
108 | def createOuterPartReduced(cls, obj):
109 | """Create a outer part which is cylinder+cone+cylinder."""
110 | dims = cls.extractDimensions(obj)
111 | aux = dims.calculateAuxiliararyPoints()
112 | # Create socket 1.
113 | r1 = dims.M / 2.0
114 | h1 = dims.bottomSocketOuterLength()
115 | cylinder1 = Part.makeCylinder(r1, h1, aux["p1"])
116 | # Create a cone and put it on the cylinder 1.
117 | r2 = dims.M1 / 2.0
118 | hc = dims.N
119 | cone = Part.makeCone(r1, r2, hc, aux["p4"])
120 | # Create a socket 2 and put it on the cone.
121 | h2 = dims.topSocketOuterLength()
122 | cylinder2 = Part.makeCylinder(r2, h2, aux["p5"])
123 | outer = cylinder1.fuse([cone, cylinder2])
124 | return outer
125 |
126 | @classmethod
127 | def createInnerPart(cls, obj):
128 | dims = cls.extractDimensions(obj)
129 | # Create parts which must be removed from the coupling.
130 | if dims.PID() == dims.PID1():
131 | return cls.createInnerPartEqual(obj)
132 | else:
133 | return cls.createInnerPartReduced(obj)
134 |
135 | @classmethod
136 | def createInnerPartEqual(cls, obj):
137 | """Create the inner part from cylinders. This is when POD and P=D1 are the equal."""
138 | dims = cls.extractDimensions(obj)
139 | aux = dims.calculateAuxiliararyPoints()
140 | # Create lower inner cylinder.
141 | height1 = dims.socketDepthA5()
142 | cylinder1i = Part.makeCylinder(dims.POD / 2.0, height1)
143 | # Create intermediatiate inner cylinder (from beginning to the end of the complete socket).
144 | height2 = dims.L
145 | cylinder2i = Part.makeCylinder(dims.PID() / 2.0, height2, aux["p1"])
146 | # Create an upper inner cylinder.
147 | cylinder3i = Part.makeCylinder(dims.POD / 2.0, height1, aux["p3"])
148 | inner = cylinder1i.fuse([cylinder2i, cylinder3i])
149 | return inner
150 |
151 | @classmethod
152 | def createInnerPartReduced(cls, obj):
153 | """Create a outer part which is cylinder+cone+cylinder."""
154 | dims = cls.extractDimensions(obj)
155 | aux = dims.calculateAuxiliararyPoints()
156 | # Create a lower cylinder.
157 | r = dims.POD / 2.0
158 | h = dims.socketDepthA5()
159 | cylinder1i = Part.makeCylinder(r, h, aux["p1"])
160 | # Create a cone and put it on the cylinder 1.
161 | r1 = dims.PID() / 2.0
162 | r2 = dims.PID1() / 2.0
163 | hc = dims.N
164 | cone = Part.makeCone(r1, r2, hc, aux["p2"])
165 | # Create an upper cylinder.
166 | r = dims.POD1 / 2
167 | h = dims.socketDepthA5()
168 | cylinder2i = Part.makeCylinder(r, h, aux["p3"])
169 | inner = cylinder1i.fuse([cone, cylinder2i])
170 | return inner
171 |
172 | def execute(self, obj):
173 | # Create the shape of the coupling.
174 | inner = Coupling.createInnerPart(obj)
175 | outer = Coupling.createOuterPart(obj)
176 | shape = outer.cut(inner)
177 | obj.Shape = shape
178 | # define Ports, i.e. where the tube have to be placed
179 | obj.Ports = self.getPorts(obj)
180 |
181 | def getPorts(self, obj):
182 | """Calculate coordinates of the ports."""
183 | dims = self.extractDimensions(obj)
184 | aux = dims.calculateAuxiliararyPoints()
185 | return [aux["p2"], aux["p3"]]
186 |
187 | @classmethod
188 | def getPortRotationAngles(cls, obj):
189 | """Calculate coordinates of the ports rotation and return them as vectorsself.
190 |
191 | x = Yaw
192 | y = Pitch
193 | z = Roll
194 | """
195 | bottom = FreeCAD.Vector(0, 90, 0)
196 | top = FreeCAD.Vector(0, -90, 0)
197 |
198 | return [bottom, top]
199 |
200 |
201 | class CouplingBuilder:
202 | """Create a coupling using Dodo/Flamingo."""
203 |
204 | def __init__(self, document):
205 | self.dims = CouplingMod.Dimensions()
206 | self.pos = FreeCAD.Vector(0, 0, 0) # Define default initial position.
207 | self.document = document
208 |
209 | def create(self, obj):
210 | """Create a couzpling.
211 |
212 | Before call it, call
213 | feature = self.document.addObject("Part::FeaturePython","OSE-Coupling")
214 | """
215 | coupling = Coupling(obj, PSize="", dims=self.dims)
216 | obj.ViewObject.Proxy = 0
217 | obj.Placement.Base = self.pos
218 |
219 | return coupling
220 |
221 |
222 | # Create a test coupling.
223 | def Test():
224 | document = FreeCAD.activeDocument()
225 | builder = CouplingBuilder(document)
226 | feature = document.addObject("Part::FeaturePython", "OSE-Coupling")
227 | builder.create(feature)
228 | document.recompute()
229 |
230 | # Test()
231 |
--------------------------------------------------------------------------------
/Resources/icons/CreateBushing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
255 |
--------------------------------------------------------------------------------
/Resources/icons/CreateTee.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
291 |
--------------------------------------------------------------------------------
/doc/adjust-ports.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
257 |
--------------------------------------------------------------------------------
/Resources/icons/MoveAround.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/OsePiping/FlElbow.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Authors: oddtopus, Ruslan Krenzler
3 | # Date: 24 March 2018
4 | # Create a elbow-fitting using Dodo/Flamingo workbench.
5 |
6 | import FreeCAD
7 | import Part
8 | # Parent class from Dodo or Flamingo.
9 | try:
10 | from pFeatures import pypeType
11 | except ModuleNotFoundError:
12 | from pipeFeatures import pypeType
13 | import OsePiping.Elbow as ElbowMod
14 |
15 |
16 | # The value RELATIVE_EPSILON is used to slightly change the size of parts
17 | # to prevent problems with boolean operations.
18 | # Keep this value very small.
19 | # For example, the outer bent part of the elbow dissaperas when it has
20 | # the same radius as the cylinder at the ends.
21 | RELATIVE_EPSILON = 0.000001
22 |
23 |
24 | class Elbow(pypeType):
25 | def __init__(self, obj, PSize="90degBend20x10", BendAngle=90, M=30, POD=20, PThk=10, H=30, J=20):
26 | # run parent __init__ and define common attributes
27 | super(Elbow, self).__init__(obj)
28 | obj.PType = "OSE_Elbow"
29 | obj.PRating = "ElbowFittingFromAnyCatalog"
30 | obj.PSize = PSize # Pipe size
31 | # define specific attributes
32 | obj.addProperty("App::PropertyLength", "M", "Elbow",
33 | "Outer diameter of the elbow.").M = M
34 | obj.addProperty("App::PropertyLength", "POD", "Elbow",
35 | "Pipe Outer Diameter.").POD = POD
36 | obj.addProperty("App::PropertyLength", "PThk", "Elbow",
37 | "Pipe wall thickness").PThk = PThk
38 | obj.addProperty("App::PropertyAngle", "BendAngle",
39 | "Elbow", "Bend Angle.").BendAngle = BendAngle
40 | obj.addProperty("App::PropertyLength", "H", "Elbow",
41 | "Distance between the center and a elbow end").H = H
42 | obj.addProperty("App::PropertyLength", "J", "Elbow",
43 | "Distnace from the center to begin of innerpart of the socket").J = J
44 |
45 | # New Dodo-pypeType already contains Ports, use them.
46 | # But if pypeType does does not have Ports, add them.
47 | if "App::PropertyVectorList" not in obj.supportedProperties():
48 | obj.addProperty("App::PropertyVectorList", "Ports", "Elbow", "Ports relative positions.")
49 | obj.Ports = self.getPorts(obj)
50 |
51 | obj.addProperty("App::PropertyVectorList", "PortRotationAngles", "Elbow",
52 | "Ports rotation angles.").PortRotationAngles = self.getPortRotationAngles(obj)
53 | obj.addProperty("App::PropertyString", "PartNumber",
54 | "Elbow", "Part number").PartNumber = ""
55 | # Make Ports read only.
56 | obj.setEditorMode("Ports", 1)
57 | obj.setEditorMode("PortRotationAngles", 1)
58 |
59 | def onChanged(self, obj, prop):
60 | # if you aim to do something when an attribute is changed
61 | # place the code here:
62 | # e.g. -> change PSize according the new alpha, PID and POD
63 |
64 | # Dimensions which can change port coordinates.
65 | dim_properties = ["BendAngle", "J"]
66 | if prop in dim_properties:
67 | # This function is called within __init__ too.
68 | # We wait for all dimension.
69 | if set(ElbowMod.DIMENSIONS_USED).issubset(obj.PropertiesList):
70 | obj.Ports = self.getPorts(obj)
71 | # Wait until PortRotationAngles are defined.
72 | if hasattr(obj, "PortRotationAngles"):
73 | obj.PortRotationAngles = self.getPortRotationAngles(obj)
74 |
75 | @staticmethod
76 | def extractDimensions(obj):
77 | dims = ElbowMod.Dimensions()
78 | dims.BendAngle = obj.BendAngle
79 | dims.H = obj.H
80 | dims.J = obj.J
81 | dims.M = obj.M
82 | dims.POD = obj.POD
83 | dims.PThk = obj.PThk
84 | return dims
85 |
86 | @staticmethod
87 | def createBentCylinderDoesNotWork(obj, rCirc):
88 | """Create a cylinder of radius rCirc in x-y plane which is bent in the middle and is streight in the ends.
89 |
90 | The little streight part is necessary, because otherwise the part is not displayed
91 | correctly after performing a boolean operations. Thus we need some overlapping
92 | between bent part and the socket.
93 |
94 | :param group: Group where to add created objects.
95 | :param rCirc: Radius of the cylinder.
96 |
97 | See documentation picture elbow-cacluations.png
98 | """
99 | # Convert alpha to degree value
100 | dims = Elbow.extractDimensions(obj)
101 |
102 | aux = dims.calculateAuxiliararyPoints()
103 |
104 | alpha = float(dims.BendAngle.getValueAs("deg"))
105 | rBend = dims.M / 2.0
106 |
107 | # Put a base on the streight part.
108 | base = Part.makeCircle(rCirc, aux["p5"], aux["p5"])
109 |
110 | # Add trajectory
111 | line1 = Part.makeLine(aux["p5"], aux["p2"])
112 | arc = Part.makeCircle(
113 | rBend, aux["p3"], FreeCAD.Vector(0, 0, 1), 225 - alpha / 2, 225 + alpha / 2)
114 | line2 = Part.makeLine(aux["p4"], aux["p6"])
115 |
116 | trajectory = Part.Shape([line1, arc, line2])
117 | # Show trajectory for debugging.
118 | # W Part.Wire([line1, arc, line2])
119 | # Part.show(W)
120 | # Add a cap (circle, at the other end of the bent cylinder).
121 | cap = Part.makeCircle(rCirc, aux["p5"], aux["p5"])
122 | # Sweep the circle along the trajectory.
123 | sweep = Part.makeSweepSurface(trajectory, base) # Does not work
124 | sweep = Part.makeSweepSurface(W, base) # Does not work.
125 | # The sweep is only a 2D service consisting of walls only.
126 | # Add circles on both ends of this wall.
127 | end1 = Part.Face(Part.Wire(base))
128 | # Create other end.
129 | end2 = Part.Face(Part.Wire(cap))
130 | solid = Part.Solid(Part.Shell([end1, sweep, end2]))
131 | return solid
132 |
133 | @staticmethod
134 | def createBentCylinder(obj, rCirc):
135 | """Create a cylinder of radius rCirc in x-y plane which is bent in the middle.
136 |
137 | :param group: Group where to add created objects.
138 | :param rCirc: Radius of the cylinder.
139 |
140 | See documentation picture elbow-cacluations.png
141 | """
142 | # Convert alpha to degree value
143 | dims = Elbow.extractDimensions(obj)
144 |
145 | aux = dims.calculateAuxiliararyPoints()
146 |
147 | alpha = float(dims.BendAngle.getValueAs("deg"))
148 | rBend = dims.M / 2.0
149 |
150 | # Put a base on the streight part.
151 | base = Part.makeCircle(rCirc, aux["p2"], aux["p2"])
152 |
153 | # Add trajectory
154 | trajectory = Part.makeCircle(
155 | rBend, aux["p3"], FreeCAD.Vector(0, 0, 1), 225 - alpha / 2, 225 + alpha / 2)
156 | # Show trajectory for debugging.
157 | # W = W1.fuse([trajectory.Edges])
158 | # Part.Show(W)
159 | # Add a cap (circle, at the other end of the bent cylinder).
160 | cap = Part.makeCircle(rCirc, aux["p4"], aux["p4"])
161 | # Sweep the circle along the trajectory.
162 | sweep = Part.makeSweepSurface(trajectory, base)
163 | # The sweep is only a 2D service consisting of walls only.
164 | # Add circles on both ends of this wall.
165 | end1 = Part.Face(Part.Wire(base))
166 | # Create other end.
167 | end2 = Part.Face(Part.Wire(cap))
168 | solid = Part.Solid(Part.Shell([end1, sweep, end2]))
169 | return solid
170 |
171 | @staticmethod
172 | def createOuterPart(obj):
173 | dims = Elbow.extractDimensions(obj)
174 | aux = dims.calculateAuxiliararyPoints()
175 | r = dims.M / 2
176 | # For unknow reasons, witoutm the factor r*0.999999 the middle part disappears.
177 | bentPart = Elbow.createBentCylinder(obj, r * (1 + RELATIVE_EPSILON))
178 | # Create socket along the z axis.
179 | h = float(dims.H) - aux["p2"].Length
180 | socket1 = Part.makeCylinder(r, h, aux["p2"], aux["p2"])
181 | # Create socket along the bent part.
182 | socket2 = Part.makeCylinder(r, h, aux["p4"], aux["p4"])
183 |
184 | outer = bentPart.fuse([socket1, socket2])
185 | return outer
186 |
187 | @staticmethod
188 | def createInnerPart(obj):
189 | dims = Elbow.extractDimensions(obj)
190 | aux = dims.calculateAuxiliararyPoints()
191 |
192 | r = dims.POD / 2 - dims.PThk
193 |
194 | bentPart = Elbow.createBentCylinder(obj, r * (1 + RELATIVE_EPSILON))
195 | # Create a channel along the z axis. It is longer then necessaryself.
196 | # But it possible can prevent problems with boolean operations.
197 | h = float(dims.H)
198 | chan1 = Part.makeCylinder(r, h, aux["p2"], aux["p2"])
199 | # Create a channel along the bent part.
200 | chan2 = Part.makeCylinder(r, h, aux["p4"], aux["p4"])
201 | # Create corresponding socktes.
202 |
203 | rSocket = dims.POD / 2
204 | # The socket length is actually dims.H - dims.J. But we do it longer
205 | # to prevent problems with bulean operations
206 | hSocket = dims.H
207 | socket1 = Part.makeCylinder(rSocket, hSocket, aux["p5"], aux["p5"])
208 | socket2 = Part.makeCylinder(rSocket, hSocket, aux["p6"], aux["p6"])
209 |
210 | inner = bentPart.fuse([chan1, chan2, socket1, socket2])
211 | return inner
212 |
213 | @staticmethod
214 | def createShape(obj):
215 | outer = Elbow.createOuterPart(obj)
216 | inner = Elbow.createInnerPart(obj)
217 | return outer.cut(inner)
218 |
219 | def execute(self, obj):
220 | # Create the shape of the elbow.
221 | shape = Elbow.createShape(obj)
222 | obj.Shape = shape
223 | # Recalculate ports.
224 | obj.Ports = self.getPorts(obj)
225 |
226 | def getPorts(self, obj):
227 | dims = Elbow.extractDimensions(obj)
228 | aux = dims.calculateAuxiliararyPoints()
229 | # FreeCAD.Console.PrintMessage("Ports are %s and %s"%(aux["p5"], aux["p6"]))
230 | return [aux["p5"], aux["p6"]]
231 |
232 | def getPortRotationAngles(self, obj):
233 | """Calculate coordinates of the ports rotation and return them as vectorsself.
234 |
235 | x = Yaw
236 | y = Pitch
237 | z = Roll
238 | """
239 | dims = Elbow.extractDimensions(obj)
240 | half = dims.BendAngle / 2
241 | # -45° and 135° are rotation of 0° elbow. They acts as a refence for a bent elbow.
242 | end0 = FreeCAD.Vector(-45 + half.Value, 0, 0)
243 | end1 = FreeCAD.Vector(135 - half.Value, 0, 0)
244 | return [end0, end1]
245 |
246 |
247 | class ElbowBuilder:
248 | """Create elbow using Dodo/Flamingo."""
249 |
250 | def __init__(self, document):
251 | self.dims = ElbowMod.Dimensions()
252 | self.pos = FreeCAD.Vector(0, 0, 0) # Define default initial position.
253 | self.document = document
254 |
255 | def create(self, obj):
256 | """Create an elbow."""
257 | elbow = Elbow(obj, PSize="", BendAngle=self.dims.BendAngle, M=self.dims.M, POD=self.dims.POD,
258 | PThk=self.dims.PThk, H=self.dims.H, J=self.dims.J)
259 | obj.ViewObject.Proxy = 0
260 | obj.Placement.Base = self.pos
261 |
262 | return elbow
263 |
264 |
265 | # Test builder.
266 | def TestElbow():
267 | document = FreeCAD.activeDocument()
268 | builder = ElbowBuilder(document)
269 | feature = document.addObject("Part::FeaturePython", "OSE-Elbow")
270 | # builder.dims.BendAngle = FreeCAD.Units.parseQuantity("90 deg")
271 | builder.create(feature)
272 | document.recompute()
273 |
274 | # TestElbow()
275 |
--------------------------------------------------------------------------------