├── .gitignore ├── Resources ├── smvideo.jpg ├── SheetMetal4.gif ├── sheetmetal_terms.png ├── SheetMetal.qrc └── icons │ ├── SheetMetal_AddCornerRelief.svg │ ├── SheetMetal_AddRelief.svg │ ├── SheetMetal_AddJunction.svg │ ├── SheetMetal_AddBend.svg │ ├── SheetMetal_Update.svg │ └── SheetMetal_Forming.svg ├── tools ├── terminology.png ├── calc-unfold.py ├── README.md └── Press_brake_schematic.svg ├── updating-ui.md ├── package.xml ├── smwb_locator.py ├── Init.py ├── engineering_mode.py ├── lookup.py ├── InitGui.py ├── SMprefs.ui ├── UnfoldOptions.ui ├── SheetMetalBendSolid.py ├── README.md ├── SheetMetalBaseCmd.py ├── Macros └── SheetMetalUnfoldUpdater.FCMacro ├── SheetMetalJunction.py ├── SheetMetalBend.py ├── SheetMetalRelief.py ├── SheetMetalFoldCmd.py ├── SketchOnSheetMetalCmd.py └── SheetMetalFormingCmd.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /SheetMetal2.py -------------------------------------------------------------------------------- /Resources/smvideo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyw/FreeCAD_SheetMetal/master/Resources/smvideo.jpg -------------------------------------------------------------------------------- /tools/terminology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyw/FreeCAD_SheetMetal/master/tools/terminology.png -------------------------------------------------------------------------------- /Resources/SheetMetal4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyw/FreeCAD_SheetMetal/master/Resources/SheetMetal4.gif -------------------------------------------------------------------------------- /Resources/sheetmetal_terms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyw/FreeCAD_SheetMetal/master/Resources/sheetmetal_terms.png -------------------------------------------------------------------------------- /updating-ui.md: -------------------------------------------------------------------------------- 1 | # Updating UnfoldOptions.ui 2 | 3 | 1. Generate the Python code from UnfoldOptions.ui by: 4 | 5 | ``` 6 | pyuic4 UnfoldOptions.ui -o myui.py 7 | ``` 8 | 9 | 2. Copy and paste relevant codes into the `SMUnfoldTaskPanel()` class in `SheetMetalUnfolder.py`. 10 | 11 | 12 | # TODO 13 | 14 | * Use `UnfoldOptions.ui` file directly. 15 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SheetMetal Workbench 4 | A simple sheet metal tools workbench for FreeCAD. 5 | 0.2.50 6 | 2022-07-09 7 | Shai Seger 8 | GPLv3 9 | https://github.com/shaise/FreeCAD_SheetMetal 10 | https://github.com/shaise/FreeCAD_SheetMetal/blob/master/README.md 11 | Resources/icons/SMLogo.svg 12 | 13 | 14 | 15 | SMWorkbench 16 | ./ 17 | sheetmetal 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tools/calc-unfold.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Tool for manually calculating the unfolded geometry 3 | # See terminology.png for terminology. 4 | 5 | import math 6 | 7 | r = inner_radius = 1.64 8 | T = thickness = 2.0 9 | ML = mold_line_distance = 50 10 | K = k_factor = 0.38 11 | bend_angle = 90.0 12 | 13 | t = thickness * k_factor 14 | BA = bend_allowance = 2.0 * math.pi * (r + t) * (bend_angle / 360.0) 15 | leg_length = ML - BA / 2.0 16 | ossb = r + T 17 | flange_diff = ossb - BA / 2.0 18 | flange_length = ossb + leg_length 19 | 20 | print "Inputs: r, T, Kf, ML, angle" 21 | print "---------------------------------" 22 | print "Effective inner radius: %.2f mm" % inner_radius 23 | print "Effective outer radius: %.2f mm" % (r + T) 24 | print "Flange length: %.2f mm" % flange_length 25 | print "* Flange diff (FD = FL - ML): %.2f mm" % flange_diff 26 | print "Bend allowance: %.2f mm" % BA 27 | print "Leg length: %.2f mm" % leg_length 28 | -------------------------------------------------------------------------------- /Resources/SheetMetal.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/SheetMetal_AddBase.svg 4 | icons/SheetMetal_AddBend.svg 5 | icons/SheetMetal_AddCornerRelief.svg 6 | icons/SheetMetal_AddFoldWall.svg 7 | icons/SheetMetal_AddJunction.svg 8 | icons/SheetMetal_AddRelief.svg 9 | icons/SheetMetal_AddWall.svg 10 | icons/SheetMetal_Extrude.svg 11 | icons/SheetMetal_Forming.svg 12 | icons/SheetMetal_SketchOnSheet.svg 13 | icons/SheetMetal_Unfold.svg 14 | icons/SheetMetal_UnfoldUnattended.svg 15 | icons/SheetMetal_UnfoldUpdate.svg 16 | icons/SheetMetal_Update.svg 17 | icons/preferences-sheetmetal.svg 18 | icons/SMLogo.svg 19 | 20 | -------------------------------------------------------------------------------- /smwb_locator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # smwb_locator.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################## 25 | -------------------------------------------------------------------------------- /Init.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Init.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################## 25 | 26 | print("Sheet Metal workbench loaded") -------------------------------------------------------------------------------- /engineering_mode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ################################################################################### 3 | # 4 | # engineering_mode.py 5 | # 6 | # Copyright 2019 Cerem Cem Aslan 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ################################################################################### 25 | 26 | import FreeCAD 27 | 28 | 29 | def engineering_mode_enabled(): 30 | FSParam = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/SheetMetal") 31 | return FSParam.GetInt("EngineeringUXMode", 0) # 0 = disabled, 1 = enabled 32 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Air Bending 2 | 3 | ![air-bending-punch-distances](./air-bending-punch-distances.svg) 4 | 5 | 6 | ``` 7 | <- b -> <- a -> 8 | ----+------+-------| 9 | B A 10 | ``` 11 | 12 | ## First bend (A) 13 | 14 | x is equal to the "ML" distance, which is "a" in this case. 15 | 16 | ## After first bend (B, ...) 17 | 18 | x is equal to "b + FD" distance. Calculate FD value with [calc-unfold](./calc-unfold.py) tool. 19 | 20 | ## K-factor table 21 | 22 | Taken from https://en.wikipedia.org/wiki/Bending_(metalworking): 23 | 24 | | Generic K-factors (ANSI) | Aluminum | Aluminum | Steel | 25 | |----------------------------|----------------|------------------|----------------| 26 | | Radius | Soft materials | Medium materials | Hard materials | 27 | | **Air bending** | | | | 28 | | 0 to thickness | 0.33 | 0.38 | 0.40 | 29 | | Thickness to 3 × thickness | 0.40 | 0.43 | 0.45 | 30 | | Greater than 3 × thickness | 0.50 | 0.50 | 0.50 | 31 | | **Bottoming** | | | | 32 | | 0 to thickness | 0.42 | 0.44 | 0.46 | 33 | | Thickness to 3 × thickness | 0.46 | 0.47 | 0.48 | 34 | | Greater than 3 × thickness | 0.50 | 0.50 | 0.50 | 35 | | **Coining** | | | | 36 | | 0 to thickness | 0.38 | 0.41 | 0.44 | 37 | | Thickness to 3 × thickness | 0.44 | 0.46 | 0.47 | 38 | | Greater than 3 × thickness | 0.50 | 0.50 | 0.50 | 39 | -------------------------------------------------------------------------------- /lookup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ################################################################################### 3 | # 4 | # lookup.py 5 | # 6 | # Copyright 2019 Cerem Cem Aslan 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ################################################################################### 25 | 26 | import collections 27 | 28 | 29 | def get_val_from_range(lookup, input, interpolate=False): 30 | ''' 31 | lookup: dictionary 32 | input: float 33 | 34 | For working principle, see below tests 35 | ''' 36 | lookup_sorted = collections.OrderedDict(sorted(lookup.items(), key=lambda t: float(t[0]))) 37 | val = None 38 | prev_val = None 39 | prev_key = None 40 | input = float(input) 41 | for _range in lookup_sorted: 42 | val = float(lookup_sorted[_range]) 43 | if input > float(_range): 44 | prev_val = val 45 | prev_key = float(_range) 46 | continue 47 | 48 | if interpolate: 49 | # Do the interpolation here 50 | if prev_key is not None: 51 | key = float(_range) 52 | #print "interpolate for input: ", input, ": ", prev_key, "to ", key, "->", prev_val, val 53 | input_offset_percentage = (input - prev_key) / (key - prev_key) 54 | val_diff = val - prev_val 55 | val_offset = val_diff * input_offset_percentage 56 | interpolated_val = prev_val + val_offset 57 | round_2 = lambda a: int((a * 100) + 0.5) / 100.0 58 | val = round_2(interpolated_val) 59 | #print "...interpolated to: ", val, interpolated_val 60 | break 61 | return val 62 | 63 | mytable = { 64 | 1: 0.25, 65 | 1.1: 0.28, 66 | 3: 0.33, 67 | 5: 0.42, 68 | 7: 0.5 69 | } 70 | 71 | # Interpolation disabled 72 | assert get_val_from_range(mytable, 0.1) == 0.25 73 | assert get_val_from_range(mytable, 0.99) == 0.25 74 | assert get_val_from_range(mytable, 1) == 0.25 75 | assert get_val_from_range(mytable, 1.01) == 0.28 76 | assert get_val_from_range(mytable, 1.09) == 0.28 77 | assert get_val_from_range(mytable, 1.2) == 0.33 78 | assert get_val_from_range(mytable, 2.5) == 0.33 79 | assert get_val_from_range(mytable, 4) == 0.42 80 | assert get_val_from_range(mytable, 40) == 0.5 81 | assert get_val_from_range(mytable, 1000) == 0.5 82 | 83 | # Interpolation enabled 84 | assert get_val_from_range(mytable, 0.1, True) == 0.25 85 | assert get_val_from_range(mytable, 0.99, True) == 0.25 86 | assert get_val_from_range(mytable, 1, True) == 0.25 87 | assert get_val_from_range(mytable, 1.01, True) == 0.25 88 | assert get_val_from_range(mytable, 1.09, True) == 0.28 89 | assert get_val_from_range(mytable, 2.05, True) == 0.31 90 | assert get_val_from_range(mytable, 2.5, True) == 0.32 91 | assert get_val_from_range(mytable, 4, True) == 0.38 92 | assert get_val_from_range(mytable, 6, True) == 0.46 93 | assert get_val_from_range(mytable, 40, True) == 0.5 94 | assert get_val_from_range(mytable, 1000, True) == 0.5 95 | -------------------------------------------------------------------------------- /InitGui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # InitGui.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################## 25 | 26 | from PySide import QtCore 27 | import smwb_locator 28 | import os 29 | from engineering_mode import engineering_mode_enabled 30 | 31 | smWBpath = os.path.dirname(smwb_locator.__file__) 32 | smWB_icons_path = os.path.join( smWBpath, 'Resources', 'icons') 33 | 34 | global main_smWB_Icon 35 | main_smWB_Icon = os.path.join( smWB_icons_path , 'SMLogo.svg') 36 | 37 | class SMWorkbench (Workbench): 38 | 39 | global main_smWB_Icon 40 | global SHEETMETALWB_VERSION 41 | global QtCore 42 | global engineering_mode_enabled 43 | global smWBpath 44 | global smWB_icons_path 45 | 46 | MenuText = 'Sheet Metal' 47 | ToolTip = QtCore.QT_TRANSLATE_NOOP('SheetMetal','Sheet Metal workbench allows for designing and unfolding sheet metal parts') 48 | Icon = main_smWB_Icon 49 | 50 | def Initialize(self): 51 | "This function is executed when FreeCAD starts" 52 | import SheetMetalCmd # import here all the needed files that create your FreeCAD commands 53 | import SheetMetalExtendCmd 54 | import SheetMetalUnfolder 55 | import SheetMetalBaseCmd 56 | import SheetMetalFoldCmd 57 | import SheetMetalRelief 58 | import SheetMetalJunction 59 | import SheetMetalBend 60 | import SketchOnSheetMetalCmd 61 | import SheetMetalCornerReliefCmd 62 | import SheetMetalFormingCmd 63 | import os.path 64 | self.list = ["SMBase", "SMMakeWall", "SMExtrudeFace", "SMFoldWall", "SMUnfold", "SMCornerRelief", "SMMakeRelief", "SMMakeJunction", 65 | "SMMakeBend", "SMSketchOnSheet", "SMFormingWall"] # A list of command names created in the line above 66 | if engineering_mode_enabled(): 67 | self.list.insert(self.list.index("SMUnfold") + 1,"SMUnfoldUnattended") 68 | self.appendToolbar("Sheet Metal",self.list) # creates a new toolbar with your commands 69 | self.appendMenu("Sheet Metal",self.list) # creates a new menu 70 | # self.appendMenu(["An existing Menu","My submenu"],self.list) # appends a submenu to an existing menu 71 | FreeCADGui.addPreferencePage(os.path.join(smWBpath, 'SMprefs.ui'),'SheetMetal') 72 | FreeCADGui.addIconPath(smWB_icons_path) 73 | 74 | def Activated(self): 75 | "This function is executed when the workbench is activated" 76 | return 77 | 78 | def Deactivated(self): 79 | "This function is executed when the workbench is deactivated" 80 | return 81 | 82 | def ContextMenu(self, recipient): 83 | "This is executed whenever the user right-clicks on screen" 84 | # "recipient" will be either "view" or "tree" 85 | self.appendContextMenu("Sheet Metal",self.list) # add commands to the context menu 86 | 87 | def GetClassName(self): 88 | # this function is mandatory if this is a full python workbench 89 | return "Gui::PythonWorkbench" 90 | 91 | Gui.addWorkbench(SMWorkbench()) 92 | -------------------------------------------------------------------------------- /SMprefs.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gui::Dialog::DlgSettingsDraft 4 | 5 | 6 | 7 | 0 8 | 0 9 | 563 10 | 401 11 | 12 | 13 | 14 | General settings 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 1 23 | 24 | 25 | 26 | General 27 | 28 | 29 | 30 | 31 | 32 | 0 33 | 34 | 35 | 36 | 37 | Engineering UX Mode 38 | 39 | 40 | 41 | 42 | 43 | 44 | Qt::Horizontal 45 | 46 | 47 | 48 | 40 49 | 20 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | true 58 | 59 | 60 | Method of icon grouping 61 | 62 | 63 | 0 64 | 65 | 66 | EngineeringUXMode 67 | 68 | 69 | Mod/SheetMetal 70 | 71 | 72 | 73 | Disabled 74 | 75 | 76 | 77 | 78 | Enabled 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Qt::Vertical 89 | 90 | 91 | 92 | 20 93 | 40 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 0 106 | 0 107 | 108 | 109 | 110 | 111 | 16777215 112 | 30 113 | 114 | 115 | 116 | 117 | 14 118 | 119 | 120 | 121 | Preferences for the SheetMetal Workbench 122 | 123 | 124 | 125 | 126 | 127 | 128 | qPixmapFromMimeSource 129 | 130 | 131 | Gui::PrefComboBox 132 | QComboBox 133 |
Gui/PrefWidgets.h
134 |
135 |
136 | 137 | 138 |
139 | -------------------------------------------------------------------------------- /Resources/icons/SheetMetal_AddCornerRelief.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Resources/icons/SheetMetal_AddRelief.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Resources/icons/SheetMetal_AddJunction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Resources/icons/SheetMetal_AddBend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /UnfoldOptions.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SMUnfoldTaskPanel 4 | 5 | 6 | 7 | 0 8 | 0 9 | 462 10 | 415 11 | 12 | 13 | 14 | Unfold sheet metal object 15 | 16 | 17 | 18 | 19 | 20 | QLayout::SetMaximumSize 21 | 22 | 23 | 24 | 25 | Generate projection sketch 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Use Material Definition Sheet 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 46 | 16777215 47 | 48 | 49 | 50 | Apply 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | QLayout::SetDefaultConstraint 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 0 67 | 68 | 69 | 70 | Manual K-factor 71 | 72 | 73 | 74 | 75 | 76 | 77 | Qt::Horizontal 78 | 79 | 80 | 81 | 40 82 | 20 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | false 91 | 92 | 93 | 94 | 0 95 | 0 96 | 97 | 98 | 99 | 100 | 70 101 | 16777215 102 | 103 | 104 | 105 | 3 106 | 107 | 108 | 2.000000000000000 109 | 110 | 111 | 0.100000000000000 112 | 113 | 114 | 0.500000000000000 115 | 116 | 117 | 118 | 119 | 120 | 121 | ANSI 122 | 123 | 124 | 125 | 126 | 127 | 128 | DIN 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | QLayout::SetMinimumSize 138 | 139 | 140 | 0 141 | 142 | 143 | 144 | 145 | Unfold object transparency 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 0 154 | 0 155 | 156 | 157 | 158 | 159 | 70 160 | 16777215 161 | 162 | 163 | 164 | % 165 | 166 | 167 | 100 168 | 169 | 170 | 70 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | Qt::Vertical 180 | 181 | 182 | 183 | 20 184 | 40 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /SheetMetalBendSolid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # SheetMetalBendSolid.py 5 | # 6 | # Copyright 2020 Jaise James 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################## 25 | import Part, math 26 | 27 | def getPointOnCylinder(zeroVert, poi, Radius, circent, axis, zeroVertNormal): 28 | dist = zeroVert.distanceToPlane(poi, zeroVertNormal) 29 | poi1 = poi.projectToPlane(zeroVert, zeroVertNormal) 30 | angle = dist / Radius 31 | axis = axis * -1 32 | #print([dist, angle]) 33 | vec = poi1 - circent 34 | rVec = vec * math.cos(angle) + axis * ( axis.dot(vec)) * (1 - math.cos(angle)) + vec.cross(axis) * math.sin(angle) 35 | Point = circent + rVec 36 | #print([rVec,Point]) 37 | return Point 38 | 39 | def WrapBSpline(bspline, Radius, zeroVert, cent, axis, zeroVertNormal): 40 | poles = bspline.getPoles() 41 | newpoles = [] 42 | for poi in poles: 43 | bPoint = getPointOnCylinder(zeroVert, poi, Radius, cent, axis, zeroVertNormal) 44 | newpoles.append(bPoint) 45 | newbspline = bspline 46 | newbspline.buildFromPolesMultsKnots(newpoles, bspline.getMultiplicities(), 47 | bspline.getKnots(), bspline.isPeriodic(), bspline.Degree, bspline.getWeights()) 48 | return newbspline.toShape() 49 | 50 | def WrapFace(Face, Radius, axis, normal, zeroVert, cent, zeroVertNormal): 51 | # circent = cent 52 | Edges = [] 53 | for e in Face.Edges: 54 | #print(type(e.Curve)) 55 | if isinstance(e.Curve, (Part.Circle, Part.ArcOfCircle)) : 56 | #bspline = gg.Curve.toBSpline() 57 | poles = e.discretize(Number=50) 58 | bspline=Part.BSplineCurve() 59 | bspline.interpolate(poles) 60 | #bs = bspline.toShape() 61 | #Part.show(bs,"bspline") 62 | Edges.append(WrapBSpline(bspline, Radius, zeroVert, cent, axis, zeroVertNormal)) 63 | 64 | elif isinstance(e.Curve, Part.BSplineCurve) : 65 | Edges.append(WrapBSpline(e.Curve, Radius, zeroVert, cent, axis, zeroVertNormal)) 66 | 67 | elif isinstance(e.Curve, Part.Line) : 68 | sp = e.valueAt(e.FirstParameter) 69 | ep = e.valueAt(e.LastParameter) 70 | dist1 = abs(sp.distanceToPlane(cent, normal)) 71 | dist2 = abs(ep.distanceToPlane(cent, normal)) 72 | #print(dist1,dist2) 73 | linenormal = ep - sp 74 | mp = sp + linenormal / 2.0 75 | linenormal.normalize() 76 | #print(linenormal.dot(axis)) 77 | #print(linenormal.dot(normal)) 78 | if linenormal.dot(axis) == 0.0 and (dist2 - dist1) == 0.0: 79 | Point1 = getPointOnCylinder(zeroVert, sp, Radius, cent, axis, zeroVertNormal) 80 | Point2 = getPointOnCylinder(zeroVert, mp, Radius, cent, axis, zeroVertNormal) 81 | Point3 = getPointOnCylinder(zeroVert, ep, Radius, cent, axis, zeroVertNormal) 82 | arc = Part.Arc(Point1,Point2,Point3) 83 | Edges.append(arc.toShape()) 84 | elif linenormal.dot(axis) == 1.0 or linenormal.dot(axis) == -1.0 : 85 | Point1 = getPointOnCylinder(zeroVert, sp, Radius, cent, axis, zeroVertNormal) 86 | Point2 = getPointOnCylinder(zeroVert, ep, Radius, cent, axis, zeroVertNormal) 87 | #print([Point1,Point2]) 88 | Edges.append(Part.makeLine(Point1, Point2)) 89 | elif linenormal.dot(normal) == 1.0 or linenormal.dot(normal) == -1.0 : 90 | Point1 = getPointOnCylinder(zeroVert, sp, Radius, cent, axis, zeroVertNormal) 91 | Point2 = getPointOnCylinder(zeroVert, ep, Radius, cent, axis, zeroVertNormal) 92 | #print([Point1,Point2]) 93 | Edges.append(Part.makeLine(Point1, Point2)) 94 | else : 95 | poles = e.discretize(Number=50) 96 | #print(poles) 97 | bspline=Part.BSplineCurve() 98 | bspline.interpolate(poles, PeriodicFlag=False) 99 | #bs = bspline.toShape() 100 | #Part.show(bs,"bspline") 101 | #bspline = disgg.toBSpline() 102 | Edges.append(WrapBSpline(bspline, Radius, zeroVert, cent, axis, zeroVertNormal)) 103 | return Edges 104 | 105 | def BendSolid(SelFace, SelEdge, BendR, thk, neutralRadius, Axis, flipped): 106 | normal = SelFace.normalAt(0,0) 107 | zeroVert = SelEdge.Vertexes[0].Point 108 | if not(flipped) : 109 | cent = zeroVert + normal * BendR 110 | zeroVertNormal = normal.cross(Axis) * -1 111 | shp = Part.makeCylinder(BendR, 100, cent, Axis, 360) 112 | else: 113 | cent = zeroVert - normal * (BendR + thk) 114 | zeroVertNormal = normal.cross(Axis) 115 | shp = Part.makeCylinder(BendR+thk, 100, cent, Axis, 360) 116 | elt = shp.Face1 117 | #Part.show(elt) 118 | #Part.show(SelFace) 119 | #print([cent,zeroVertNormal]) 120 | 121 | # Wirelist = [] 122 | w = WrapFace(SelFace, neutralRadius, Axis, normal, zeroVert, cent, zeroVertNormal) 123 | eList = Part.__sortEdges__(w) 124 | myWire = Part.Wire(eList) 125 | #Part.show(myWire, "myWire") 126 | nextFace = Part.Face(elt.Surface, myWire) 127 | #f.check(True) 128 | nextFace.validate() 129 | #Part.show(nextFace, "nextFace") 130 | nextFace.check(True) # No output = good 131 | #Part.show(nextFace, "nextFace") 132 | if not(flipped) : 133 | bendsolid = nextFace.makeOffsetShape(thk, 0.0, fill = True) 134 | else: 135 | bendsolid = nextFace.makeOffsetShape(-thk, 0.0, fill = True) 136 | #Part.show(bendsolid, "bendsolid") 137 | return bendsolid 138 | 139 | 140 | -------------------------------------------------------------------------------- /tools/Press_brake_schematic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 50 | 52 | 59 | 65 | 70 | 71 | 73 | 77 | 81 | 82 | 91 | 100 | 109 | 110 | 113 | 117 | 121 | 125 | 129 | 134 | 139 | 144 | 149 | PUNCH 159 | WORKPIECE 169 | BACK GAUGE 179 | DIE 189 | 190 | 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeCAD SheetMetal Workbench 2 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/shaise/FreeCAD_SheetMetal.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/shaise/FreeCAD_SheetMetal/alerts/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/shaise/FreeCAD_SheetMetal.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/shaise/FreeCAD_SheetMetal/context:python) 3 | 4 | A simple sheet metal tools workbench for FreeCAD 5 | 6 | ![Demo Workflow](Resources/SheetMetal4.gif) 7 | 8 | ### Tutorial by Joko Engineering: 9 | 10 | [![Tutorial](Resources/smvideo.jpg)](https://youtu.be/xidvQYkC4so "FreeCAD - The Elegant Sheet Metal Workbench") 11 | 12 | #### Developers: 13 | * Folding tools: 14 | > [@shaise](https://github.com/shaise) Shai Seger 15 | > [@jaisekjames](https://github.com/jaisekjames) 16 | > [@ceremcem](https://github.com/ceremcem) Cerem Cem ASLAN 17 | > ([@JMG1](https://github.com/JMG1)) Based and inspired by Javier Martínez's code 18 | * Unfolding tool: 19 | > Copyright 2014 by Ulrich Brammer AKA [@ulrich1a](https://github.com/ulrich1a) 20 | 21 | # Terminology 22 | ## Sheetmetal Workbench definitions 23 | ![Sheetmetal WB Terminology](Resources/sheetmetal_terms.png) 24 | 25 | ## Physical material definitions 26 | ![Physical Terminology](tools/terminology.png) 27 | 28 | # Test case 29 | 30 | As a simple test case, consider the following example: 31 | 32 | * Inputs: 33 | - Thickness: 2mm 34 | - K-factor: 0.38 (ANSI) 35 | - Leg length: 48.12mm 36 | - Inner effective radius: 1.64mm 37 | - Flange length: 51.76mm 38 | * Output: 39 | - End to mold-line distance: 50mm 40 | 41 | You can find a simple calculator in [`tools/calc-unfold.py`](tools/calc-unfold.py). 42 | 43 | # Material Definition Sheet 44 | 45 | ### Description 46 | 47 | You can use a Spreadsheet object to declare K-factor values inside the project file permanently. This will allow: 48 | 49 | * Different K-factor values to be used for each bend in your model 50 | * Sharing the same material definition for multiple objects 51 | 52 | ### Usage 53 | 54 | 1. Create a spreadsheet with the name of `material_foo` with the following content (see [this table](https://user-images.githubusercontent.com/6639874/56498031-b017bc00-6508-11e9-8b14-6076513d8488.png)): 55 | 56 | | Radius / Thickness | K-factor (ANSI) | 57 | | ---| ---| 58 | | 1 | 0.38 | 59 | | 3 | 0.43 | 60 | | 99 | 0.5 | 61 | 62 | Notes: 63 | 64 | 1. The cell names are case/space sensitive. 65 | 2. Possible values for `K-factor` is `K-factor (ANSI)` or `K-factor (DIN)`. 66 | 3. `Radius / Thickness` means `Radius over Thickness`. Eg. if inner radius is `1.64mm` and material thickness is `2mm` then `Radius / Thickness == 1.64/2 = 0.82` so `0.38` will be used as the K-factor. See [lookup.py](https://github.com/ceremcem/FreeCAD_SheetMetal/blob/k-factor-from-lookup/lookup.py#L46-L68) for more examples. 67 | 68 | 2. Use "Unfold Task Panel" to assign the material sheet. 69 | 3. Unfold as usual. 70 | 71 | ### Screencast 72 | ![Screencast](https://user-images.githubusercontent.com/6639874/56642679-a749f600-6680-11e9-944a-82e447d9dc4e.gif) 73 | 74 | # Engineering Mode 75 | 76 | ### Description 77 | 78 | Some sort of parameters effect the fabrication process but are impossible to inspect visually, such as K-factor, which makes them susceptible to go unnoticed until the actual erroneous production took place. 79 | 80 | In engineering mode, such "non-visually-inspectable" values are not assigned with default values and explicit user input is required. "Engineering mode" is a safer UX mode for production environments. 81 | 82 | ### Activating 83 | 84 | 1. Switch to SheetMetal WB at least once. 85 | 2. Edit -> Preferences -> SheetMetal 86 | 3. Select `enabled` in `Engineering UX Mode` field. 87 | 88 | # Installation 89 | For installation and how to use, please visit: 90 | http://theseger.com/projects/2015/06/sheet-metal-addon-for-freecad/ 91 | Starting from FreeCAD 0.17 it can be installed via the [Addon Manager](https://github.com/FreeCAD/FreeCAD-addons) (from Tools menu) 92 | 93 | #### References 94 | * Development repo: https://github.com/shaise/FreeCAD_SheetMetal 95 | * FreeCAD wiki page: https://www.freecadweb.org/wiki/SheetMetal_Workbench 96 | * Authors webpage: http://theseger.com/projects/2015/06/sheet-metal-addon-for-freecad/ 97 | * FreeCAD Forum announcement/discussion [thread](https://forum.freecadweb.org/viewtopic.php?f=3&t=60818) 98 | 99 | #### Release notes: 100 | * V0.2.50 09 Jul 2022: Moved 'Drawing' to 'TechDraw' for FC0.21 compatibility. Thank you! 101 | * V0.2.49 03 Jul 2021: Add SubShapeBinder as source by [@s-light][s-light]. Thank you! 102 | * V0.2.48 02 May 2021: Add context menu [@jaisejames][jaisejames]. Thank you! 103 | * V0.2.47 24 Feb 2021: Add translation support by [@jaisejames][jaisejames]. Thank you! 104 | * V0.2.46 31 Jan 2021: Small bug fixes and code clean by [@jaisejames][jaisejames]. Thank you! 105 | * V0.2.45 24 Dec 2020: Added punch tool feature by [@jaisejames][jaisejames]. Thank you! 106 | * V0.2.44 19 Dec 2020: Added extend feature by [@jaisejames][jaisejames]. Thank you! 107 | * V0.2.43 01 Dec 2020: Added corner feature and map sketch to cut openings by [@jaisejames][jaisejames]. Thank you! 108 | * V0.2.42 09 Jun 2020: Added Engineering UX Mode by [@ceremcem][ceremcem]. Thank you! 109 | * V0.2.41 01 Jun 2020: Added Drop down Menu 110 | * V0.2.40 24 May 2020: Added added tools for conversion of solid corners to sheetmetal by [@jaisejames][jaisejames]. Thank you! 111 | * V0.2.34 09 Mar 2020: Rename "my commands" context menu to sheet metal 112 | * V0.2.33 09 Mar 2020: Fix bend radius bug on sketch bends. Thank you Léo Flaventin! 113 | * V0.2.32 02 Jan 2020: Python 3.8 update by [@looooo][lorenz]. Thank you! 114 | * V0.2.31 24 Apr 2019: Added better K factor control by [@ceremcem][ceremcem]. Thank you! 115 | * V0.2.30 30 Mar 2019: Added Fold-on-sketch-line tool by [@jaisejames][jaisejames]. Thank you! 116 | * V0.2.22 24 Jan 2019: Fix some typos, Issue [#54][54] 117 | * V0.2.21 20 Jan 2019: Fix some typos, Issue [#52][52] 118 | * V0.2.20 10 Jan 2019: Added sheetmetal generation from base wire by [@jaisejames][jaisejames]. Thank you! 119 | * V0.2.10 01 Nov 2018: Merge new features by [@jaisejames][jaisejames]. Thank you! 120 | * Added Edge based selection 121 | * Added Auto-mitering 122 | * Added Sketch based Wall 123 | * Added Sketch based Guided wall 124 | * Added Relief factor 125 | * Added Material Inside, thk inside, Offset options 126 | * V0.2.04 21 Sep 2018: Fix K-Factor bug 127 | * V0.2.03 20 Sep 2018: Merge [@easyw][easyw] PR: Add separate color for inner sketch lines. (issue [#46][46]). Change Gui layout 128 | * V0.2.02 15 Sep 2018: Add color selection for unfold sketches (issue [#41][41]) 129 | * V0.2.01 15 Sep 2018: 130 | * Fix bug when not generating sketch (issue [#42][42]) 131 | * Support separate color for bend lines (issue [#41][41]) 132 | * V0.2.00 04 Sep 2018: Make SheetMetal compatible with Python 3 and QT 5 133 | * V0.1.40 20 Aug 2018: Merge [Ulrich][ulrich]'s V20 unfolder script - supports many more sheet metal cases and more robust 134 | * V0.1.32 25 Jun 2018: New feature: Option to separately unfold bends. Thank you [@jaisejames][jaisejames]! 135 | * V0.1.31 25 Jun 2018: Support ellipses and parabolas, Try standard sketch conversion first 136 | * V0.1.30 25 Jun 2018: 137 | * New feature: Generate unfold sketch with folding marks. Issue [#33][33]. Thank you [@easyw][easyw]! 138 | * New feature: K-Factor foe unfolding is now editable. Issue [#30][30] 139 | * V0.1.21 19 Jun 2018: Fixed back negative bend angles, restrict miter to +/- 80 degrees 140 | * V0.1.20 19 Jun 2018: (Thank you [@jaisejames][jaisejames] for all these new features!!) 141 | * Add bend extension to make the bended wall wider 142 | * Add relief shape selection (rounded or flat) 143 | * Double clicking on a bent in the tree view, brings a dialog to select different faces (good when editing the base object breaks the bend, and new faces need to be selected) 144 | * Setting miter angle now works with unfold command 145 | * V0.1.13 10 May 2018: Change unbending method so shape refinement can work. 146 | * V0.1.12 25 Mar 2018: Allow negative bend angles. Change XPM icons to SVG 147 | * V0.1.11 01 Feb 2018: Fix Issue [#23][23]: when there is a gap only on one side, an extra face is added to the other 148 | * V0.1.10 11 Nov 2017: Add miter option to bends. By [@jaisejames][jaisejames] 149 | * V0.1.02 22 Jun 2017: Fix nesting bug, when saving and loading file 150 | * V0.1.01 03 Mar 2017: Support version 0.17 (starting from build 10423) 151 | * V0.0.13 07 Sep 2015: Add negative gaps for extrude function. (per deveee request) 152 | * V0.012 07 Sep 2015: Fix issue submitted by deveee 153 | * V0.010 13 Jun 2015: Add [Ulrich][ulrich]'s great unfolding tool. Thanks!!! 154 | * V0.002 12 Jun 2015: Fix Save/Load issues 155 | * V0.001 11 Jun 2015: Initial version 156 | 157 | [lorenz]: https://github.com/looooo 158 | [ulrich]: https://github.com/ulrich1a 159 | [ceremcem]: https://github.com/ceremcem 160 | [jaisejames]: https://github.com/jaisekjames 161 | [easyw]: https://github.com/easyw 162 | [s-light]: https://github.com/s-light 163 | [30]: https://github.com/shaise/FreeCAD_SheetMetal/issues/30 164 | [33]: https://github.com/shaise/FreeCAD_SheetMetal/issues/33 165 | [41]: https://github.com/shaise/FreeCAD_SheetMetal/issues/41 166 | [42]: https://github.com/shaise/FreeCAD_SheetMetal/issues/42 167 | [46]: https://github.com/shaise/FreeCAD_SheetMetal/issues/46 168 | [52]: https://github.com/shaise/FreeCAD_SheetMetal/issues/52 169 | [54]: https://github.com/shaise/FreeCAD_SheetMetal/issues/54 170 | 171 | ## License 172 | GPLv3 (see [LICENSE](LICENSE)) 173 | -------------------------------------------------------------------------------- /SheetMetalBaseCmd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ################################################################################### 3 | # 4 | # SheetMetalBaseCmd.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ################################################################################### 25 | 26 | from FreeCAD import Gui 27 | from PySide import QtCore, QtGui 28 | 29 | import FreeCAD, Part, os 30 | __dir__ = os.path.dirname(__file__) 31 | iconPath = os.path.join( __dir__, 'Resources', 'icons' ) 32 | 33 | def smWarnDialog(msg): 34 | diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, 'Error in macro MessageBox', msg) 35 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 36 | diag.exec_() 37 | 38 | def smBelongToBody(item, body): 39 | if (body is None): 40 | return False 41 | for obj in body.Group: 42 | if obj.Name == item.Name: 43 | return True 44 | return False 45 | 46 | def smIsSketchObject(obj): 47 | return str(obj).find(" 1: 107 | wallSolid = solidlist[0].multiFuse(solidlist[1:]) 108 | else : 109 | wallSolid = solidlist[0] 110 | #Part.show(wallSolid,"wallSolid") 111 | 112 | #Part.show(wallSolid,"wallSolid") 113 | return wallSolid 114 | 115 | class SMBaseBend: 116 | def __init__(self, obj): 117 | '''"Add wall or Wall with radius bend" ''' 118 | selobj = Gui.Selection.getSelectionEx()[0] 119 | 120 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Bend Radius") 121 | obj.addProperty("App::PropertyLength","radius","Parameters",_tip_).radius = 1.0 122 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Thickness of sheetmetal") 123 | obj.addProperty("App::PropertyLength","thickness","Parameters",_tip_).thickness = 1.0 124 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Relief Type") 125 | obj.addProperty("App::PropertyEnumeration", "BendSide", "Parameters",_tip_).BendSide = ["Outside", "Inside", "Middle"] 126 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Length of wall") 127 | obj.addProperty("App::PropertyLength","length","Parameters",_tip_).length = 100.0 128 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Wall Sketch object") 129 | obj.addProperty("App::PropertyLink", "BendSketch", "Parameters",_tip_).BendSketch = selobj.Object 130 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Extrude Symmetric to Plane") 131 | obj.addProperty("App::PropertyBool","MidPlane","Parameters",_tip_).MidPlane = False 132 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Reverse Extrusion Direction") 133 | obj.addProperty("App::PropertyBool","Reverse","Parameters",_tip_).Reverse = False 134 | obj.Proxy = self 135 | 136 | def execute(self, fp): 137 | '''"Print a short message when doing a recomputation, this method is mandatory" ''' 138 | if (not hasattr(fp,"MidPlane")): 139 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Extrude Symmetric to Plane") 140 | fp.addProperty("App::PropertyBool","MidPlane","Parameters",_tip_).MidPlane = False 141 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Reverse Extrusion Direction") 142 | fp.addProperty("App::PropertyBool","Reverse","Parameters",_tip_).Reverse = False 143 | 144 | s = smBase(thk = fp.thickness.Value, length = fp.length.Value, radius = fp.radius.Value, Side = fp.BendSide, 145 | midplane = fp.MidPlane, reverse = fp.Reverse, MainObject = fp.BendSketch) 146 | 147 | fp.Shape = s 148 | obj = Gui.ActiveDocument.getObject(fp.BendSketch.Name) 149 | if obj: 150 | obj.Visibility = False 151 | 152 | class SMBaseViewProvider: 153 | "A View provider that nests children objects under the created one" 154 | 155 | def __init__(self, obj): 156 | obj.Proxy = self 157 | self.Object = obj.Object 158 | 159 | def attach(self, obj): 160 | self.Object = obj.Object 161 | return 162 | 163 | def updateData(self, fp, prop): 164 | return 165 | 166 | def getDisplayModes(self,obj): 167 | modes=[] 168 | return modes 169 | 170 | def setDisplayMode(self,mode): 171 | return mode 172 | 173 | def onChanged(self, vp, prop): 174 | return 175 | 176 | def __getstate__(self): 177 | # return {'ObjectName' : self.Object.Name} 178 | return None 179 | 180 | def __setstate__(self,state): 181 | if state is not None: 182 | import FreeCAD 183 | doc = FreeCAD.ActiveDocument #crap 184 | self.Object = doc.getObject(state['ObjectName']) 185 | 186 | def claimChildren(self): 187 | objs = [] 188 | if hasattr(self.Object,"BendSketch"): 189 | objs.append(self.Object.BendSketch) 190 | return objs 191 | 192 | def getIcon(self): 193 | return os.path.join( iconPath , 'SheetMetal_AddBase.svg') 194 | 195 | class AddBaseCommandClass(): 196 | """Add Base Wall command""" 197 | 198 | def GetResources(self): 199 | return {'Pixmap' : os.path.join( iconPath , 'SheetMetal_AddBase.svg'), # the name of a svg file available in the resources 200 | 'MenuText': QtCore.QT_TRANSLATE_NOOP('SheetMetal','Make Base Wall'), 201 | 'Accel': "C, B", 202 | 'ToolTip' : QtCore.QT_TRANSLATE_NOOP('SheetMetal','Create a sheetmetal wall from a sketch\n' 203 | '1. Select a Skech to create bends with walls.\n' 204 | '2. Use Property editor to modify other parameters')} 205 | 206 | def Activated(self): 207 | doc = FreeCAD.ActiveDocument 208 | view = Gui.ActiveDocument.ActiveView 209 | activeBody = None 210 | # selobj = Gui.Selection.getSelectionEx()[0].Object 211 | if hasattr(view,'getActiveObject'): 212 | activeBody = view.getActiveObject('pdbody') 213 | # if not smIsOperationLegal(activeBody, selobj): 214 | # return 215 | doc.openTransaction("BaseBend") 216 | if activeBody is None : 217 | a = doc.addObject("Part::FeaturePython","BaseBend") 218 | SMBaseBend(a) 219 | SMBaseViewProvider(a.ViewObject) 220 | else: 221 | #FreeCAD.Console.PrintLog("found active body: " + activeBody.Name) 222 | a = doc.addObject("PartDesign::FeaturePython","BaseBend") 223 | SMBaseBend(a) 224 | SMBaseViewProvider(a.ViewObject) 225 | activeBody.addObject(a) 226 | doc.recompute() 227 | doc.commitTransaction() 228 | return 229 | 230 | def IsActive(self): 231 | if len(Gui.Selection.getSelection()) != 1 : 232 | return False 233 | selobj = Gui.Selection.getSelection()[0] 234 | if not( 235 | selobj.isDerivedFrom("Sketcher::SketchObject") 236 | or selobj.isDerivedFrom("PartDesign::ShapeBinder") 237 | or selobj.isDerivedFrom("PartDesign::SubShapeBinder") 238 | ): 239 | return False 240 | return True 241 | 242 | Gui.addCommand('SMBase',AddBaseCommandClass()) 243 | -------------------------------------------------------------------------------- /Resources/icons/SheetMetal_Update.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 24 | 28 | 32 | 33 | 36 | 40 | 44 | 45 | 47 | 51 | 55 | 56 | 66 | 75 | 77 | 81 | 85 | 86 | 97 | 100 | 104 | 108 | 109 | 111 | 115 | 119 | 120 | 131 | 140 | 149 | 150 | 170 | 177 | 178 | 180 | 181 | 183 | image/svg+xml 184 | 186 | 187 | 188 | 189 | [wmayer] 190 | 191 | 192 | Arch_Add 193 | 2011-10-10 194 | http://www.freecadweb.org/wiki/index.php?title=Artwork 195 | 196 | 197 | FreeCAD 198 | 199 | 200 | FreeCAD/src/Mod/Arch/Resources/icons/Arch_Add.svg 201 | 202 | 203 | FreeCAD LGPL2+ 204 | 205 | 206 | https://www.gnu.org/copyleft/lesser.html 207 | 208 | 209 | [agryson] Alexander Gryson 210 | 211 | 212 | 213 | 214 | 215 | 219 | 229 | 232 | 238 | 244 | 250 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /Macros/SheetMetalUnfoldUpdater.FCMacro: -------------------------------------------------------------------------------- 1 | __author__ = "ceremcem" 2 | """ 3 | URL : https://github.com/shaise/FreeCAD_SheetMetal/blob/master/Macros/SheetMetalUnfoldUpdater.FCMacro 4 | URL2 : https://github.com/ceremcem/FreeCAD_SheetMetal/blob/master/Macros/SheetMetalUnfoldUpdater.FCMacro 5 | 6 | Description: 7 | ----------------------------------------------------------------------------------- 8 | This macro automatically updates all unfold operations as well as their related 9 | TechDraw views. 10 | 11 | Dependencies: 12 | ----------------------------------------------------------------------------------- 13 | It depends on LinkStage3 toponaming feature for storing "unfold face" information 14 | within the project file. 15 | 16 | Usage: 17 | ----------------------------------------------------------------------------------- 18 | 19 | 1. Store the "unfold face" information within the project: 20 | 1. Put your processed (final) object into an assembly container. 21 | 2. Create an "Element" for the unfold face 22 | 3. Rename the element to "foo_unfold_face" 23 | 2. Run this macro. 24 | 3. See "foo_unfold" and "foo_unfold_solid" is created properly. 25 | 4. Create TechDraw views by using those two objects 26 | 5. (Re-)run this script. If everything (unfold, TechDraw source updates) went successfully, 27 | then those two objects should be marked as invisible. 28 | 29 | Verification: 30 | ------------------------ 31 | If everything went successful, you SHOULD NOT see any visible objects in the drawing area 32 | or any `_TMP` postfixed objects in the treeview. 33 | 34 | KNOWN ISSUE: 35 | -------------------- 36 | Problem: Following error might be received: 37 | 38 | No graphical interface 39 | discretizing Sketch 40 | Running the Python command 'SMUnfoldUnattended' failed: 41 | Traceback (most recent call last): 42 | File "/home/aea/.FreeCAD/Mod/sheetmetal/SheetMetalUnfolder.py", line 3102, in Activated 43 | taskd.accept() 44 | File "/home/aea/.FreeCAD/Mod/sheetmetal/SheetMetalUnfolder.py", line 2968, in accept 45 | docG.getObject(a.Name).Transparency = genObjTransparency 46 | 47 | Workaround: Please double click the documents that contains the bent parts at least one time. 48 | 49 | 50 | 51 | """ 52 | 53 | import FreeCAD 54 | import re 55 | 56 | echo = FreeCAD.Console.PrintMessage 57 | warn = FreeCAD.Console.PrintWarning 58 | error = FreeCAD.Console.PrintError 59 | 60 | prev_workbench = Gui.activeWorkbench().name() 61 | # TODO: Actually there is no need to switch to SMWorkbench after the first one. 62 | Gui.activateWorkbench("SMWorkbench") 63 | if prev_workbench: 64 | Gui.activateWorkbench(prev_workbench) 65 | 66 | unfold_sketch_regex = re.compile('.+_unfold_face') 67 | 68 | """ For debugging purposes, see https://github.com/realthunder/FreeCAD_assembly3/issues/236#issuecomment-651583969 69 | doc_name="driver_base" 70 | elem_label="wall_unfold_face" 71 | # ------------------------------------------------------------ 72 | Gui.ActiveDocument=Gui.getDocument(doc_name) 73 | App.ActiveDocument=App.getDocument(doc_name) 74 | App.setActiveDocument(App.ActiveDocument.Name) # Interesting. Why do we need to assign this manually? 75 | doc=App.ActiveDocument 76 | element = doc.getObjectsByLabel(elem_label)[0].Proxy 77 | partGroup = element.getAssembly().getPartGroup() 78 | subname = element.getElementSubname(True) 79 | Gui.Selection.addSelection(partGroup, subname) 80 | """ 81 | 82 | echo("\n\n\n") 83 | echo("--------------------- Updating All Unfold Operations ------------------------------\n") 84 | 85 | 86 | def get_related_views(doc, labels): 87 | related_views = {} 88 | for x in doc.Objects: 89 | if x.TypeId == 'TechDraw::DrawPage': 90 | for view in x.Views: 91 | try: 92 | _src = view.Source[0] 93 | except Exception as e: 94 | continue 95 | #print "source of ", view.Label, ' in ', x.Label, " is: ", _src.Label 96 | if _src.Label in labels: 97 | # This view uses our unfolded output, update this view at the end 98 | echo("* Found related TechDraw ProjGroup: %s\n" % view.Label) 99 | related_views[_src.Label] = view 100 | return related_views 101 | 102 | 103 | originals = [App.ActiveDocument, Gui.ActiveDocument] 104 | try: 105 | for doc_name, doc in App.listDocuments().items(): 106 | unfold_objects = list(map(lambda x: x.Label, filter( 107 | lambda x: re.match('Unfold.*$', x.Label), doc.Objects))) 108 | if unfold_objects: 109 | error("Document '%s' contains objects with Unfold* prefix, please rename them: %s\n" % 110 | (doc_name, ', '.join(unfold_objects))) 111 | raise Exception("INFO: Stopping before a possible conflict.") 112 | 113 | for doc_name, doc in App.listDocuments().items(): 114 | Gui.ActiveDocument = Gui.getDocument(doc_name) 115 | App.ActiveDocument = App.getDocument(doc_name) 116 | # Interesting. Why do we need to assign this manually? 117 | App.setActiveDocument(App.ActiveDocument.Name) 118 | 119 | for o in doc.Objects: 120 | try: 121 | _o = o.TypeId 122 | except: 123 | # probably we deleted this object before updating the existing unfold sketch. 124 | continue 125 | 126 | # find any Asm3 Element that matches with our magic postfix 127 | if o.TypeId == 'Part::FeaturePython': 128 | match = unfold_sketch_regex.match(o.Label) 129 | if match: 130 | output_name = o.Label[:-5] # remove the "_face" postfix 131 | output_name_solid = output_name + '_solid' 132 | echo("+++ In: %s, Unfold job: %s \n" % 133 | (doc_name, output_name)) 134 | 135 | related_views = get_related_views( 136 | doc, [output_name, output_name_solid]) 137 | related_view_count = len(related_views.keys()) 138 | if related_view_count > 0: 139 | echo( 140 | "* Found %d TechDraw views containing that object.\n" % related_view_count) 141 | else: 142 | warn( 143 | "* No TechDraw views found related to %s, this isn't supposed to happen.\n" % (output_name)) 144 | 145 | # backup current unfolded outputs 146 | tmp_postfix = "_TMP" 147 | old = None 148 | old_solid = None 149 | need_recomputation = False 150 | try: 151 | old = doc.getObjectsByLabel(output_name)[0] 152 | old.Label += tmp_postfix 153 | need_recomputation = True 154 | except: 155 | pass 156 | try: 157 | old_solid = doc.getObjectsByLabel(output_name_solid)[0] 158 | old_solid.Label += tmp_postfix 159 | need_recomputation = True 160 | except: 161 | pass 162 | 163 | if need_recomputation: 164 | doc.recompute() 165 | 166 | # Unfold the part 167 | Gui.Selection.clearSelection() 168 | 169 | # Get the unfold face id 170 | face_elem_label = output_name + '_face' 171 | face_elem = doc.getObjectsByLabel(face_elem_label)[0].Proxy 172 | partGroup = face_elem.getAssembly().getPartGroup() 173 | subname = face_elem.getElementSubname(True) 174 | Gui.Selection.addSelection(partGroup, subname) 175 | 176 | # Unfold 177 | Gui.runCommand('SMUnfoldUnattended') 178 | 179 | try: 180 | # Check if unfold operation is successful 181 | unfold_objects = list(map(lambda x: x.Label, filter( 182 | lambda x: re.match('Unfold.*$', x.Label), doc.Objects))) 183 | if not unfold_objects: 184 | raise Exception( 185 | "Can't unfold the sheetmetal object. Can you unfold manually?") 186 | else: 187 | Gui.Selection.clearSelection() 188 | 189 | # Get the newest object's names 190 | new_solid_name = list(map(lambda x: x.Label, filter( 191 | lambda x: re.match('Unfold[0-9]*$', x.Label), doc.Objects))) 192 | new_solid_name.sort(reverse=True) 193 | new_solid_name = new_solid_name[0] 194 | 195 | new_sketch_name = list(map(lambda x: x.Label, filter( 196 | lambda x: re.match('Unfold_Sketch[0-9]*$', x.Label), doc.Objects))) 197 | new_sketch_name.sort(reverse=True) 198 | new_sketch_name = new_sketch_name[0] 199 | 200 | # Solid object is useful for laser cut operations 201 | solid = doc.getObjectsByLabel(new_solid_name)[0] 202 | solid.Label = output_name_solid 203 | 204 | sketch = doc.getObjectsByLabel(new_sketch_name)[0] 205 | sketch.Label = output_name 206 | 207 | # Update the source of related views 208 | for name, view in related_views.items(): 209 | if name == sketch.Label: 210 | obj = sketch 211 | elif name == solid.Label: 212 | obj = solid 213 | echo("* %s is used to update %s\n" % 214 | (obj.Label, view.Label)) 215 | view.Source = [obj] 216 | # set visibility to false if a related view is found and updated. 217 | obj.Visibility = False 218 | 219 | # remove the temporary object 220 | needs_recomputation = False 221 | for o in [old, old_solid]: 222 | if o is not None: 223 | #echo("removing the old object: %s\n" % o.Name) 224 | doc.removeObject(o.Name) 225 | needs_recomputation = True 226 | if needs_recomputation: 227 | doc.recompute() 228 | 229 | except Exception as e: 230 | error("* %s (in %s): Something went wrong, restoring the previous sketches...\n" % 231 | (output_name, doc.Name)) 232 | # For debugging purposes: 233 | error('Error on line %s: %s, %s.\n' % 234 | (sys.exc_info()[-1].tb_lineno, type(e).__name__, e)) 235 | 236 | #raise Exception("Breaking the script execution before CLEANUP for debugging purposes\n") 237 | 238 | needs_recomputation = False 239 | for o in [old, old_solid]: 240 | if o is not None: 241 | orig_label = o.Label[:-(len(tmp_postfix))] 242 | warn( 243 | "!! %s: Restoring the previous object.\n" % orig_label) 244 | o.Label = orig_label 245 | o.Visibility = True 246 | needs_recomputation = True 247 | if needs_recomputation: 248 | doc.recompute() 249 | 250 | except Exception as e: 251 | warn("Script isn't terminated normally: %s\n" % e) 252 | finally: 253 | doc.recompute() 254 | App.ActiveDocument, Gui.ActiveDocument = originals 255 | App.setActiveDocument(App.ActiveDocument.Name) 256 | -------------------------------------------------------------------------------- /SheetMetalJunction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ################################################################################### 3 | # 4 | # SheetMetalJunction.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ################################################################################### 25 | 26 | from FreeCAD import Gui 27 | from PySide import QtCore, QtGui 28 | 29 | import FreeCAD, FreeCADGui, Part, os 30 | __dir__ = os.path.dirname(__file__) 31 | iconPath = os.path.join( __dir__, 'Resources', 'icons' ) 32 | smEpsilon = 0.0000001 33 | 34 | # IMPORTANT: please remember to change the element map version in case of any 35 | # changes in modeling logic 36 | smElementMapVersion = 'sm1.' 37 | 38 | def smWarnDialog(msg): 39 | diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, 'Error in macro MessageBox', msg) 40 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 41 | diag.exec_() 42 | 43 | def smBelongToBody(item, body): 44 | if (body is None): 45 | return False 46 | for obj in body.Group: 47 | if obj.Name == item.Name: 48 | return True 49 | return False 50 | 51 | def smIsPartDesign(obj): 52 | return str(obj).find(" 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################## 25 | 26 | from FreeCAD import Gui 27 | from PySide import QtCore, QtGui 28 | 29 | import FreeCAD, FreeCADGui, Part, os 30 | __dir__ = os.path.dirname(__file__) 31 | iconPath = os.path.join( __dir__, 'Resources', 'icons' ) 32 | smEpsilon = 0.0000001 33 | 34 | # IMPORTANT: please remember to change the element map version in case of any 35 | # changes in modeling logic 36 | smElementMapVersion = 'sm1.' 37 | 38 | def smWarnDialog(msg): 39 | diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, 'Error in macro MessageBox', msg) 40 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 41 | diag.exec_() 42 | 43 | def smBelongToBody(item, body): 44 | if (body is None): 45 | return False 46 | for obj in body.Group: 47 | if obj.Name == item.Name: 48 | return True 49 | return False 50 | 51 | def smIsPartDesign(obj): 52 | return str(obj).find(" filletedface.Area : 80 | filletedface = joinface.makeFillet(radius, joinface.Edges) 81 | #Part.show(filletedface,'filletedface') 82 | 83 | cutface = filletedface.cut(facelist[0]) 84 | cutface = cutface.cut(facelist[1]) 85 | #Part.show(cutface1,'cutface1') 86 | if filletedoffsetface.Area > filletedface.Area : 87 | offsetsolid1 = cutface.makeOffsetShape(-thk, 0.0, fill = True) 88 | #Part.show(offsetsolid1,'offsetsolid1') 89 | else: 90 | offsetsolid1 = cutface.makeOffsetShape(-thk, 0.0, fill = True) 91 | #Part.show(offsetsolid1,'offsetsolid1') 92 | cutsolid = BOPTools.JoinAPI.cutout_legacy(resultSolid, offsetsolid1, 0.0) 93 | #Part.show(cutsolid,'cutsolid') 94 | offsetsolid1 = cutsolid.fuse(offsetsolid1) 95 | #Part.show(offsetsolid1,'offsetsolid1') 96 | resultSolid = BOPTools.JoinAPI.cutout_legacy(resultSolid, offsetsolid1, 0.0) 97 | #Part.show(resultsolid,'resultsolid') 98 | resultSolid = resultSolid.fuse(offsetsolid1) 99 | #Part.show(resultsolid,'resultsolid') 100 | 101 | return resultSolid 102 | 103 | class SMSolidBend: 104 | def __init__(self, obj): 105 | '''"Add Bend to Solid" ''' 106 | selobj = Gui.Selection.getSelectionEx()[0] 107 | 108 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Bend Radius") 109 | obj.addProperty("App::PropertyLength","radius","Parameters", _tip_).radius = 1.0 110 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Thickness of sheetmetal") 111 | obj.addProperty("App::PropertyLength","thickness","Parameters", _tip_).thickness = 1.0 112 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Base object") 113 | obj.addProperty("App::PropertyLinkSub", "baseObject", "Parameters", _tip_).baseObject = (selobj.Object, selobj.SubElementNames) 114 | obj.Proxy = self 115 | 116 | def getElementMapVersion(self, _fp, ver, _prop, restored): 117 | if not restored: 118 | return smElementMapVersion + ver 119 | 120 | def execute(self, fp): 121 | '''"Print a short message when doing a recomputation, this method is mandatory" ''' 122 | # pass selected object shape 123 | Main_Object = fp.baseObject[0].Shape.copy() 124 | s = smSolidBend(thk = fp.thickness.Value, radius = fp.radius.Value, selEdgeNames = fp.baseObject[1], 125 | MainObject = Main_Object) 126 | fp.Shape = s 127 | fp.baseObject[0].ViewObject.Visibility = False 128 | 129 | 130 | class SMBendViewProviderTree: 131 | "A View provider that nests children objects under the created one" 132 | 133 | def __init__(self, obj): 134 | obj.Proxy = self 135 | self.Object = obj.Object 136 | 137 | def attach(self, obj): 138 | self.Object = obj.Object 139 | return 140 | 141 | def setupContextMenu(self, viewObject, menu): 142 | action = menu.addAction(FreeCAD.Qt.translate("QObject", "Edit %1").replace("%1", viewObject.Object.Label)) 143 | action.triggered.connect(lambda: self.startDefaultEditMode(viewObject)) 144 | return False 145 | 146 | def startDefaultEditMode(self, viewObject): 147 | document = viewObject.Document.Document 148 | if not document.HasPendingTransaction: 149 | text = FreeCAD.Qt.translate("QObject", "Edit %1").replace("%1", viewObject.Object.Label) 150 | document.openTransaction(text) 151 | viewObject.Document.setEdit(viewObject.Object, 0) 152 | 153 | def updateData(self, fp, prop): 154 | return 155 | 156 | def getDisplayModes(self,obj): 157 | modes=[] 158 | return modes 159 | 160 | def setDisplayMode(self,mode): 161 | return mode 162 | 163 | def onChanged(self, vp, prop): 164 | return 165 | 166 | def __getstate__(self): 167 | # return {'ObjectName' : self.Object.Name} 168 | return None 169 | 170 | def __setstate__(self,state): 171 | if state is not None: 172 | import FreeCAD 173 | doc = FreeCAD.ActiveDocument #crap 174 | self.Object = doc.getObject(state['ObjectName']) 175 | 176 | def claimChildren(self): 177 | objs = [] 178 | if hasattr(self.Object,"baseObject"): 179 | objs.append(self.Object.baseObject[0]) 180 | return objs 181 | 182 | def getIcon(self): 183 | return os.path.join( iconPath , 'SheetMetal_AddBend.svg') 184 | 185 | def setEdit(self,vobj,mode): 186 | taskd = SMBendTaskPanel() 187 | taskd.obj = vobj.Object 188 | taskd.update() 189 | self.Object.ViewObject.Visibility=False 190 | self.Object.baseObject[0].ViewObject.Visibility=True 191 | FreeCADGui.Control.showDialog(taskd) 192 | return True 193 | 194 | def unsetEdit(self,vobj,mode): 195 | FreeCADGui.Control.closeDialog() 196 | self.Object.baseObject[0].ViewObject.Visibility=False 197 | self.Object.ViewObject.Visibility=True 198 | return False 199 | 200 | class SMBendViewProviderFlat: 201 | "A View provider that nests children objects under the created one" 202 | 203 | def __init__(self, obj): 204 | obj.Proxy = self 205 | self.Object = obj.Object 206 | 207 | def attach(self, obj): 208 | self.Object = obj.Object 209 | return 210 | 211 | def updateData(self, fp, prop): 212 | return 213 | 214 | def getDisplayModes(self,obj): 215 | modes=[] 216 | return modes 217 | 218 | def setDisplayMode(self,mode): 219 | return mode 220 | 221 | def onChanged(self, vp, prop): 222 | return 223 | 224 | def __getstate__(self): 225 | # return {'ObjectName' : self.Object.Name} 226 | return None 227 | 228 | def __setstate__(self,state): 229 | if state is not None: 230 | import FreeCAD 231 | doc = FreeCAD.ActiveDocument #crap 232 | self.Object = doc.getObject(state['ObjectName']) 233 | 234 | def claimChildren(self): 235 | 236 | return [] 237 | 238 | def getIcon(self): 239 | return os.path.join( iconPath , 'SheetMetal_AddBend.svg') 240 | 241 | def setEdit(self,vobj,mode): 242 | taskd = SMBendTaskPanel() 243 | taskd.obj = vobj.Object 244 | taskd.update() 245 | self.Object.ViewObject.Visibility=False 246 | self.Object.baseObject[0].ViewObject.Visibility=True 247 | FreeCADGui.Control.showDialog(taskd) 248 | return True 249 | 250 | def unsetEdit(self,vobj,mode): 251 | FreeCADGui.Control.closeDialog() 252 | self.Object.baseObject[0].ViewObject.Visibility=False 253 | self.Object.ViewObject.Visibility=True 254 | return False 255 | 256 | class SMBendTaskPanel: 257 | '''A TaskPanel for the Sheetmetal''' 258 | def __init__(self): 259 | 260 | self.obj = None 261 | self.form = QtGui.QWidget() 262 | self.form.setObjectName("SMBendTaskPanel") 263 | self.form.setWindowTitle("Binded faces/edges list") 264 | self.grid = QtGui.QGridLayout(self.form) 265 | self.grid.setObjectName("grid") 266 | self.title = QtGui.QLabel(self.form) 267 | self.grid.addWidget(self.title, 0, 0, 1, 2) 268 | self.title.setText("Select new face(s)/Edge(s) and press Update") 269 | 270 | # tree 271 | self.tree = QtGui.QTreeWidget(self.form) 272 | self.grid.addWidget(self.tree, 1, 0, 1, 2) 273 | self.tree.setColumnCount(2) 274 | self.tree.setHeaderLabels(["Name","Subelement"]) 275 | 276 | # buttons 277 | self.addButton = QtGui.QPushButton(self.form) 278 | self.addButton.setObjectName("addButton") 279 | self.addButton.setIcon(QtGui.QIcon(os.path.join( iconPath , 'SheetMetal_Update.svg'))) 280 | self.grid.addWidget(self.addButton, 3, 0, 1, 2) 281 | 282 | QtCore.QObject.connect(self.addButton, QtCore.SIGNAL("clicked()"), self.updateElement) 283 | self.update() 284 | 285 | def isAllowedAlterSelection(self): 286 | return True 287 | 288 | def isAllowedAlterView(self): 289 | return True 290 | 291 | def getStandardButtons(self): 292 | return int(QtGui.QDialogButtonBox.Ok) 293 | 294 | def update(self): 295 | 'fills the treewidget' 296 | self.tree.clear() 297 | if self.obj: 298 | f = self.obj.baseObject 299 | if isinstance(f[1],list): 300 | for subf in f[1]: 301 | #FreeCAD.Console.PrintLog("item: " + subf + "\n") 302 | item = QtGui.QTreeWidgetItem(self.tree) 303 | item.setText(0,f[0].Name) 304 | item.setIcon(0,QtGui.QIcon(":/icons/Tree_Part.svg")) 305 | item.setText(1,subf) 306 | else: 307 | item = QtGui.QTreeWidgetItem(self.tree) 308 | item.setText(0,f[0].Name) 309 | item.setIcon(0,QtGui.QIcon(":/icons/Tree_Part.svg")) 310 | item.setText(1,f[1][0]) 311 | self.retranslateUi(self.form) 312 | 313 | def updateElement(self): 314 | if self.obj: 315 | sel = FreeCADGui.Selection.getSelectionEx()[0] 316 | if sel.HasSubObjects: 317 | obj = sel.Object 318 | for elt in sel.SubElementNames: 319 | if "Face" in elt or "Edge" in elt: 320 | face = self.obj.baseObject 321 | found = False 322 | if (face[0] == obj.Name): 323 | if isinstance(face[1],tuple): 324 | for subf in face[1]: 325 | if subf == elt: 326 | found = True 327 | else: 328 | if (face[1][0] == elt): 329 | found = True 330 | if not found: 331 | self.obj.baseObject = (sel.Object, sel.SubElementNames) 332 | self.update() 333 | 334 | def accept(self): 335 | FreeCAD.ActiveDocument.recompute() 336 | FreeCADGui.ActiveDocument.resetEdit() 337 | #self.obj.ViewObject.Visibility=True 338 | return True 339 | 340 | def retranslateUi(self, TaskPanel): 341 | #TaskPanel.setWindowTitle(QtGui.QApplication.translate("draft", "Faces", None)) 342 | self.addButton.setText(QtGui.QApplication.translate("draft", "Update", None)) 343 | 344 | 345 | class AddBendCommandClass(): 346 | """Add Solid Bend command""" 347 | 348 | def GetResources(self): 349 | return {'Pixmap' : os.path.join( iconPath , 'SheetMetal_AddBend.svg'), # the name of a svg file available in the resources 350 | 'MenuText': QtCore.QT_TRANSLATE_NOOP('SheetMetal','Make Bend'), 351 | 'Accel': "S, B", 352 | 'ToolTip' : QtCore.QT_TRANSLATE_NOOP('SheetMetal','Create Bend where two walls come together on solids\n' 353 | '1. Select edge(s) to create bend on corner edge(s).\n' 354 | '2. Use Property editor to modify parameters')} 355 | 356 | def Activated(self): 357 | doc = FreeCAD.ActiveDocument 358 | view = Gui.ActiveDocument.ActiveView 359 | activeBody = None 360 | selobj = Gui.Selection.getSelectionEx()[0].Object 361 | if hasattr(view,'getActiveObject'): 362 | activeBody = view.getActiveObject('pdbody') 363 | if not smIsOperationLegal(activeBody, selobj): 364 | return 365 | doc.openTransaction("Add Bend") 366 | if activeBody is None or not smIsPartDesign(selobj): 367 | a = doc.addObject("Part::FeaturePython","SolidBend") 368 | SMSolidBend(a) 369 | SMBendViewProviderTree(a.ViewObject) 370 | else: 371 | #FreeCAD.Console.PrintLog("found active body: " + activeBody.Name) 372 | a = doc.addObject("PartDesign::FeaturePython","SolidBend") 373 | SMSolidBend(a) 374 | SMBendViewProviderFlat(a.ViewObject) 375 | activeBody.addObject(a) 376 | FreeCADGui.Selection.clearSelection() 377 | doc.recompute() 378 | doc.commitTransaction() 379 | return 380 | 381 | def IsActive(self): 382 | if len(Gui.Selection.getSelection()) < 1 or len(Gui.Selection.getSelectionEx()[0].SubElementNames) < 1: 383 | return False 384 | # selobj = Gui.Selection.getSelection()[0] 385 | for selFace in Gui.Selection.getSelectionEx()[0].SubObjects: 386 | if type(selFace) != Part.Edge : 387 | return False 388 | return True 389 | 390 | Gui.addCommand('SMMakeBend',AddBendCommandClass()) 391 | 392 | -------------------------------------------------------------------------------- /Resources/icons/SheetMetal_Forming.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Shape2DView 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /SheetMetalRelief.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################### 3 | # 4 | # SheetMetalRelief.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################### 25 | 26 | from FreeCAD import Gui 27 | from PySide import QtCore, QtGui 28 | 29 | import FreeCAD, FreeCADGui, Part, os 30 | __dir__ = os.path.dirname(__file__) 31 | iconPath = os.path.join( __dir__, 'Resources', 'icons' ) 32 | smEpsilon = 0.0000001 33 | 34 | # IMPORTANT: please remember to change the element map version in case of any 35 | # changes in modeling logic 36 | smElementMapVersion = 'sm1.' 37 | 38 | def smWarnDialog(msg): 39 | diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, 'Error in macro MessageBox', msg) 40 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 41 | diag.exec_() 42 | 43 | def smBelongToBody(item, body): 44 | if (body is None): 45 | return False 46 | for obj in body.Group: 47 | if obj.Name == item.Name: 48 | return True 49 | return False 50 | 51 | def smIsPartDesign(obj): 52 | return str(obj).find(" 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################## 25 | 26 | from FreeCAD import Gui 27 | from PySide import QtCore, QtGui 28 | 29 | import FreeCAD, Part, os, math 30 | __dir__ = os.path.dirname(__file__) 31 | iconPath = os.path.join( __dir__, 'Resources', 'icons' ) 32 | smEpsilon = 0.0000001 33 | 34 | import SheetMetalBendSolid 35 | 36 | def smWarnDialog(msg): 37 | diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, 'Error in macro MessageBox', msg) 38 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 39 | diag.exec_() 40 | 41 | def smBelongToBody(item, body): 42 | if (body is None): 43 | return False 44 | for obj in body.Group: 45 | if obj.Name == item.Name: 46 | return True 47 | return False 48 | 49 | def smIsPartDesign(obj): 50 | return str(obj).find(" 0.0 : 96 | foldface = FoldShape.getElement(selFaceNames[0]) 97 | tool = bendlinesketch.Shape.copy() 98 | normal = foldface.normalAt(0,0) 99 | thk = smthk(FoldShape, foldface) 100 | print(thk) 101 | 102 | # if not(flipped) : 103 | #offset = thk * kfactor 104 | #else : 105 | #offset = thk * (1 - kfactor ) 106 | 107 | unfoldLength = ( bendR + kfactor * thk ) * bendA * math.pi / 180.0 108 | neutralRadius = ( bendR + kfactor * thk ) 109 | #neutralLength = ( bendR + kfactor * thk ) * math.tan(math.radians(bendA / 2.0)) * 2.0 110 | #offsetdistance = neutralLength - unfoldLength 111 | #scalefactor = neutralLength / unfoldLength 112 | #print([neutralRadius, neutralLength, unfoldLength, offsetdistance, scalefactor]) 113 | 114 | #To get facedir 115 | toolFaces = tool.extrude(normal * -thk) 116 | #Part.show(toolFaces, "toolFaces") 117 | cutSolid = BOPTools.SplitAPI.slice(FoldShape, toolFaces.Faces, "Standard", 0.0) 118 | #Part.show(cutSolid,"cutSolid_check") 119 | 120 | if not(invertbend) : 121 | solid0 = cutSolid.childShapes()[0] 122 | else : 123 | solid0 = cutSolid.childShapes()[1] 124 | 125 | cutFaceDir = smCutFace(toolFaces.Faces[0], solid0) 126 | #Part.show(cutFaceDir,"cutFaceDir") 127 | facenormal = cutFaceDir.Faces[0].normalAt(0,0) 128 | #print(facenormal) 129 | 130 | if position == "middle" : 131 | tool.translate(facenormal * -unfoldLength / 2.0 ) 132 | toolFaces = tool.extrude(normal * -thk) 133 | elif position == "backward" : 134 | tool.translate(facenormal * -unfoldLength ) 135 | toolFaces = tool.extrude(normal * -thk) 136 | 137 | #To get split solid 138 | solidlist = [] 139 | toolExtr = toolFaces.extrude(facenormal * unfoldLength) 140 | #Part.show(toolExtr,"toolExtr") 141 | CutSolids = FoldShape.cut(toolExtr) 142 | #Part.show(Solids,"Solids") 143 | solid2list, solid1list = [], [] 144 | for solid in CutSolids.Solids : 145 | checksolid = toolFaces.common(solid) 146 | if checksolid.Faces : 147 | solid1list.append(solid) 148 | else : 149 | solid2list.append(solid) 150 | 151 | if len(solid1list) > 1 : 152 | solid0 = solid1list[0].multiFuse(solid1list[1:]) 153 | else : 154 | solid0 = solid1list[0] 155 | #Part.show(solid0,"solid0") 156 | 157 | if len(solid2list) > 1 : 158 | solid1 = solid2list[0].multiFuse(solid2list[1:]) 159 | else : 160 | solid1 = solid2list[0] 161 | #Part.show(solid0,"solid0") 162 | #Part.show(solid1,"solid1") 163 | 164 | bendEdges = FoldShape.common(tool) 165 | #Part.show(bendEdges,"bendEdges") 166 | bendEdge = bendEdges.Edges[0] 167 | if not(flipped) : 168 | revAxisP = bendEdge.valueAt(bendEdge.FirstParameter) + normal * bendR 169 | else : 170 | revAxisP = bendEdge.valueAt(bendEdge.FirstParameter) - normal * (thk + bendR) 171 | revAxisV = bendEdge.valueAt(bendEdge.LastParameter) - bendEdge.valueAt(bendEdge.FirstParameter) 172 | revAxisV.normalize() 173 | 174 | # To check sktech line direction 175 | if (normal.cross(revAxisV).normalize() - facenormal).Length > smEpsilon: 176 | revAxisV = revAxisV * -1 177 | #print(revAxisV) 178 | 179 | if flipped : 180 | revAxisV = revAxisV * -1 181 | #print(revAxisV) 182 | 183 | # To get bend surface 184 | # revLine = Part.LineSegment(tool.Vertexes[0].Point, tool.Vertexes[-1].Point ).toShape() 185 | # bendSurf = revLine.revolve(revAxisP, revAxisV, bendA) 186 | #Part.show(bendSurf,"bendSurf") 187 | 188 | # bendSurfTest = bendSurf.makeOffsetShape(bendR/2.0, 0.0, fill = False) 189 | # #Part.show(bendSurfTest,"bendSurfTest") 190 | # offset = 1 191 | # if bendSurfTest.Area < bendSurf.Area and not(flipped) : 192 | # offset = -1 193 | # elif bendSurfTest.Area > bendSurf.Area and flipped : 194 | # offset = -1 195 | # #print(offset) 196 | 197 | # To get bend solid 198 | flatsolid = FoldShape.cut(solid0) 199 | flatsolid = flatsolid.cut( solid1) 200 | #Part.show(flatsolid,"flatsolid") 201 | flatfaces = foldface.common(flatsolid) 202 | #Part.show(flatfaces,"flatface") 203 | solid1.translate(facenormal * (-unfoldLength)) 204 | #Part.show(solid1,"solid1") 205 | solid1.rotate(revAxisP, revAxisV, bendA) 206 | #Part.show(solid1,"rotatedsolid1") 207 | # bendSolidlist =[] 208 | for flatface in flatfaces.Faces : 209 | bendsolid = SheetMetalBendSolid.BendSolid(flatface, bendEdge, bendR, thk, neutralRadius, revAxisV, flipped) 210 | #Part.show(bendsolid,"bendsolid") 211 | solidlist.append(bendsolid) 212 | 213 | solidlist.append(solid0) 214 | solidlist.append(solid1) 215 | #resultsolid = Part.makeCompound(solidlist) 216 | #resultsolid = BOPTools.JoinAPI.connect(solidlist) 217 | resultsolid = solidlist[0].multiFuse(solidlist[1:]) 218 | 219 | else : 220 | if bendlinesketch and bendA > 0.0 : 221 | resultsolid = FoldShape 222 | 223 | Gui.ActiveDocument.getObject(MainObject.Name).Visibility = False 224 | Gui.ActiveDocument.getObject(bendlinesketch.Name).Visibility = False 225 | return resultsolid 226 | 227 | class SMFoldWall: 228 | def __init__(self, obj): 229 | '''"Fold / Bend a Sheetmetal with given Bend Radius" ''' 230 | selobj = Gui.Selection.getSelectionEx() 231 | 232 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Bend Radius") 233 | obj.addProperty("App::PropertyLength","radius","Parameters",_tip_).radius = 1.0 234 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Bend Angle") 235 | obj.addProperty("App::PropertyAngle","angle","Parameters",_tip_).angle = 90.0 236 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Base Object") 237 | obj.addProperty("App::PropertyLinkSub", "baseObject", "Parameters",_tip_).baseObject = (selobj[0].Object, selobj[0].SubElementNames) 238 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Bend Reference Line List") 239 | obj.addProperty("App::PropertyLink","BendLine","Parameters",_tip_).BendLine = selobj[1].Object 240 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Invert Solid Bend Direction") 241 | obj.addProperty("App::PropertyBool","invertbend","Parameters",_tip_).invertbend = False 242 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Neutral Axis Position") 243 | obj.addProperty("App::PropertyFloatConstraint","kfactor","Parameters",_tip_).kfactor = (0.5,0.0,1.0,0.01) 244 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Invert Bend Direction") 245 | obj.addProperty("App::PropertyBool","invert","Parameters",_tip_).invert = False 246 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Unfold Bend") 247 | obj.addProperty("App::PropertyBool","unfold","Parameters",_tip_).unfold = False 248 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Bend Line Position") 249 | obj.addProperty("App::PropertyEnumeration", "Position", "Parameters",_tip_).Position = ["forward", "middle", "backward"] 250 | obj.Proxy = self 251 | 252 | def execute(self, fp): 253 | '''"Print a short message when doing a recomputation, this method is mandatory" ''' 254 | 255 | if (not hasattr(fp,"Position")): 256 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Bend Line Position") 257 | fp.addProperty("App::PropertyEnumeration", "Position", "Parameters",_tip_).Position = ["forward", "middle", "backward"] 258 | 259 | s = smFold(bendR = fp.radius.Value, bendA = fp.angle.Value, flipped = fp.invert, unfold = fp.unfold, kfactor = fp.kfactor, bendlinesketch = fp.BendLine, 260 | position = fp.Position, invertbend = fp.invertbend, selFaceNames = fp.baseObject[1], MainObject = fp.baseObject[0]) 261 | fp.Shape = s 262 | 263 | class SMFoldViewProvider: 264 | "A View provider that nests children objects under the created one" 265 | 266 | def __init__(self, obj): 267 | obj.Proxy = self 268 | self.Object = obj.Object 269 | 270 | def attach(self, obj): 271 | self.Object = obj.Object 272 | return 273 | 274 | def updateData(self, fp, prop): 275 | return 276 | 277 | def getDisplayModes(self,obj): 278 | modes=[] 279 | return modes 280 | 281 | def setDisplayMode(self,mode): 282 | return mode 283 | 284 | def onChanged(self, vp, prop): 285 | return 286 | 287 | def __getstate__(self): 288 | # return {'ObjectName' : self.Object.Name} 289 | return None 290 | 291 | def __setstate__(self,state): 292 | if state is not None: 293 | import FreeCAD 294 | doc = FreeCAD.ActiveDocument #crap 295 | self.Object = doc.getObject(state['ObjectName']) 296 | 297 | def claimChildren(self): 298 | objs = [] 299 | if hasattr(self.Object,"baseObject"): 300 | objs.append(self.Object.baseObject[0]) 301 | objs.append(self.Object.BendLine) 302 | return objs 303 | 304 | def getIcon(self): 305 | return os.path.join( iconPath , 'SheetMetal_AddFoldWall.svg') 306 | 307 | class SMFoldPDViewProvider: 308 | "A View provider that nests children objects under the created one" 309 | 310 | def __init__(self, obj): 311 | obj.Proxy = self 312 | self.Object = obj.Object 313 | 314 | def attach(self, obj): 315 | self.Object = obj.Object 316 | return 317 | 318 | def updateData(self, fp, prop): 319 | return 320 | 321 | def getDisplayModes(self,obj): 322 | modes=[] 323 | return modes 324 | 325 | def setDisplayMode(self,mode): 326 | return mode 327 | 328 | def onChanged(self, vp, prop): 329 | return 330 | 331 | def __getstate__(self): 332 | # return {'ObjectName' : self.Object.Name} 333 | return None 334 | 335 | def __setstate__(self,state): 336 | if state is not None: 337 | import FreeCAD 338 | doc = FreeCAD.ActiveDocument #crap 339 | self.Object = doc.getObject(state['ObjectName']) 340 | 341 | def claimChildren(self): 342 | objs = [] 343 | if hasattr(self.Object,"BendLine"): 344 | objs.append(self.Object.BendLine) 345 | return objs 346 | 347 | def getIcon(self): 348 | return os.path.join( iconPath , 'SheetMetal_AddFoldWall.svg') 349 | 350 | class AddFoldWallCommandClass(): 351 | """Add Fold Wall command""" 352 | 353 | def GetResources(self): 354 | return {'Pixmap' : os.path.join( iconPath , 'SheetMetal_AddFoldWall.svg'), # the name of a svg file available in the resources 355 | 'MenuText': QtCore.QT_TRANSLATE_NOOP('SheetMetal','Fold a Wall'), 356 | 'Accel': "C, F", 357 | 'ToolTip' : QtCore.QT_TRANSLATE_NOOP('SheetMetal','Fold a wall of metal sheet\n' 358 | '1. Select a flat face on sheet metal and\n' 359 | '2. Select a bend line(sketch) on same face(size more than face) to create sheetmetal fold.\n' 360 | '3. Use Property editor to modify other parameters')} 361 | 362 | def Activated(self): 363 | doc = FreeCAD.ActiveDocument 364 | view = Gui.ActiveDocument.ActiveView 365 | activeBody = None 366 | selobj = Gui.Selection.getSelectionEx()[0].Object 367 | if hasattr(view,'getActiveObject'): 368 | activeBody = view.getActiveObject('pdbody') 369 | if not smIsOperationLegal(activeBody, selobj): 370 | return 371 | doc.openTransaction("Bend") 372 | if activeBody is None or not smIsPartDesign(selobj): 373 | a = doc.addObject("Part::FeaturePython","Fold") 374 | SMFoldWall(a) 375 | SMFoldViewProvider(a.ViewObject) 376 | else: 377 | #FreeCAD.Console.PrintLog("found active body: " + activeBody.Name) 378 | a = doc.addObject("PartDesign::FeaturePython","Fold") 379 | SMFoldWall(a) 380 | SMFoldPDViewProvider(a.ViewObject) 381 | activeBody.addObject(a) 382 | doc.recompute() 383 | doc.commitTransaction() 384 | return 385 | 386 | def IsActive(self): 387 | if len(Gui.Selection.getSelection()) < 2 : 388 | return False 389 | selFace = Gui.Selection.getSelectionEx()[0].SubObjects[0] 390 | if type(selFace) != Part.Face: 391 | return False 392 | selobj = Gui.Selection.getSelection()[1] 393 | if not(selobj.isDerivedFrom('Sketcher::SketchObject')) : 394 | return False 395 | return True 396 | 397 | Gui.addCommand('SMFoldWall',AddFoldWallCommandClass()) 398 | -------------------------------------------------------------------------------- /SketchOnSheetMetalCmd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # SketchOnSheetMetalCmd.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ############################################################################## 25 | 26 | from FreeCAD import Gui 27 | from PySide import QtCore, QtGui 28 | 29 | import FreeCAD, Part, os, math 30 | __dir__ = os.path.dirname(__file__) 31 | iconPath = os.path.join( __dir__, 'Resources', 'icons' ) 32 | smEpsilon = 0.0000001 33 | 34 | import SheetMetalBendSolid 35 | 36 | def smWarnDialog(msg): 37 | diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, 'Error in macro MessageBox', msg) 38 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 39 | diag.exec_() 40 | 41 | def smBelongToBody(item, body): 42 | if (body is None): 43 | return False 44 | for obj in body.Group: 45 | if obj.Name == item.Name: 46 | return True 47 | return False 48 | 49 | def smIsPartDesign(obj): 50 | return str(obj).find(" Facelist[1].Area : 64 | selFace = Facelist[0] 65 | else : 66 | selFace = Facelist[1] 67 | elif type(sel_item) == Part.Face : 68 | selFace = sel_item 69 | return selFace 70 | 71 | def smthk(obj, foldface) : 72 | normal = foldface.normalAt(0,0) 73 | theVol = obj.Volume 74 | if theVol < 0.0001: 75 | SMError("Shape is not a real 3D-object or to small for a metal-sheet!") 76 | else: 77 | # Make a first estimate of the thickness 78 | estimated_thk = theVol/(obj.Area / 2.0) 79 | # p1 = foldface.CenterOfMass 80 | for v in foldface.Vertexes : 81 | p1 = v.Point 82 | p2 = p1 + estimated_thk * -1.5 * normal 83 | e1 = Part.makeLine(p1, p2) 84 | thkedge = obj.common(e1) 85 | thk = thkedge.Length 86 | if thk > smEpsilon : 87 | break 88 | return thk 89 | 90 | def smCutFace(Face, obj) : 91 | # find face Modified During loop 92 | for face in obj.Faces : 93 | face_common = face.common(Face) 94 | if face_common.Faces : 95 | break 96 | return face 97 | 98 | def smGetEdge(Face, obj) : 99 | # find face Modified During loop 100 | for edge in obj.Edges : 101 | face_common = edge.common(Face) 102 | if face_common.Edges : 103 | break 104 | return edge 105 | 106 | def equal_angle(ang1, ang2, p=5): 107 | # compares two angles 108 | result = False 109 | if round(ang1 - ang2, p)==0: 110 | result = True 111 | if round((ang1-2.0*math.pi) - ang2, p)==0: 112 | result = True 113 | if round(ang1 - (ang2-2.0*math.pi), p)==0: 114 | result = True 115 | return result 116 | 117 | def bendAngle(theFace, edge_vec) : 118 | #Start to investigate the angles at self.__Shape.Faces[face_idx].ParameterRange[0] 119 | #Part.show(theFace,"theFace") 120 | #valuelist = theFace.ParameterRange 121 | #print(valuelist) 122 | angle_0 = theFace.ParameterRange[0] 123 | angle_1 = theFace.ParameterRange[1] 124 | 125 | # idea: identify the angle at edge_vec = P_edge.Vertexes[0].copy().Point 126 | # This will be = angle_start 127 | # calculate the tan_vec from valueAt 128 | edgeAngle, edgePar = theFace.Surface.parameter(edge_vec) 129 | #print('the angles: ', angle_0, ' ', angle_1, ' ', edgeAngle, ' ', edgeAngle - 2*math.pi) 130 | 131 | if equal_angle(angle_0, edgeAngle): 132 | angle_start = angle_0 133 | angle_end = angle_1 134 | else: 135 | angle_start = angle_1 136 | angle_end = angle_0 137 | 138 | bend_angle = angle_end - angle_start 139 | # angle_tan = angle_start + bend_angle/6.0 # need to have the angle_tan before correcting the sign 140 | 141 | if bend_angle < 0.0: 142 | bend_angle = -bend_angle 143 | 144 | #print(math.degrees(bend_angle)) 145 | return math.degrees(bend_angle) 146 | 147 | def smSketchOnSheetMetal(kfactor = 0.5, sketch = '', flipped = False, selFaceNames = '', MainObject = None): 148 | resultSolid = MainObject.Shape.copy() 149 | selElement = resultSolid.getElement(selFaceNames[0]) 150 | LargeFace = smFace(selElement, resultSolid) 151 | sketch_face = Part.makeFace(sketch.Shape.Wires,"Part::FaceMakerBullseye") 152 | 153 | #To get thk of sheet, top face normal 154 | thk = smthk(resultSolid, LargeFace) 155 | #print(thk) 156 | 157 | #To get top face normal, flatsolid 158 | solidlist = [] 159 | normal = LargeFace.normalAt(0,0) 160 | #To check face direction 161 | coeff = normal.dot(sketch_face.Faces[0].normalAt(0,0)) 162 | if coeff < 0 : 163 | sketch_face.reverse() 164 | Flatface = sketch_face.common(LargeFace) 165 | BalanceFaces = sketch_face.cut(Flatface) 166 | #Part.show(BalanceFace,"BalanceFace") 167 | Flatsolid = Flatface.extrude(normal * -thk) 168 | #Part.show(Flatsolid,"Flatsolid") 169 | solidlist.append(Flatsolid) 170 | 171 | if BalanceFaces.Faces : 172 | for BalanceFace in BalanceFaces.Faces : 173 | #Part.show(BalanceFace,"BalanceFace") 174 | TopFace = LargeFace 175 | #Part.show(TopFace,"TopFace") 176 | #flipped = False 177 | while BalanceFace.Faces : 178 | BendEdge = smGetEdge(BalanceFace, TopFace) 179 | #Part.show(BendEdge,"BendEdge") 180 | facelist = resultSolid.ancestorsOfType(BendEdge, Part.Face) 181 | 182 | #To get bend radius, bend angle 183 | for cylface in facelist : 184 | if issubclass(type(cylface.Surface),Part.Cylinder) : 185 | break 186 | if not(issubclass(type(cylface.Surface),Part.Cylinder)) : 187 | break 188 | #Part.show(cylface,"cylface") 189 | for planeface in facelist : 190 | if issubclass(type(planeface.Surface),Part.Plane) : 191 | break 192 | #Part.show(planeface,"planeface") 193 | normal = planeface.normalAt(0,0) 194 | revAxisV = cylface.Surface.Axis 195 | revAxisP = cylface.Surface.Center 196 | bendA = bendAngle(cylface, revAxisP) 197 | #print([bendA, revAxisV, revAxisP, cylface.Orientation]) 198 | 199 | #To check bend direction 200 | offsetface = cylface.makeOffsetShape(-thk, 0.0, fill = False) 201 | #Part.show(offsetface,"offsetface") 202 | if offsetface.Area < cylface.Area : 203 | bendR = cylface.Surface.Radius - thk 204 | flipped = True 205 | else : 206 | bendR = cylface.Surface.Radius 207 | flipped = False 208 | 209 | #To arrive unfold Length, neutralRadius 210 | unfoldLength = ( bendR + kfactor * thk ) * abs(bendA) * math.pi / 180.0 211 | neutralRadius = ( bendR + kfactor * thk ) 212 | #print([unfoldLength,neutralRadius]) 213 | 214 | #To get faceNormal, bend face 215 | faceNormal = normal.cross(revAxisV).normalize() 216 | #print(faceNormal) 217 | if bendR < cylface.Surface.Radius : 218 | offsetSolid = cylface.makeOffsetShape(bendR/2.0, 0.0, fill = True) 219 | else: 220 | offsetSolid = cylface.makeOffsetShape(-bendR/2.0, 0.0, fill = True) 221 | #Part.show(offsetSolid,"offsetSolid") 222 | tool = BendEdge.copy() 223 | FaceArea = tool.extrude(faceNormal * -unfoldLength ) 224 | #Part.show(FaceArea,"FaceArea") 225 | #Part.show(BalanceFace,"BalanceFace") 226 | SolidFace = offsetSolid.common(FaceArea) 227 | #Part.show(BendSolidFace,"BendSolidFace") 228 | if not(SolidFace.Faces): 229 | faceNormal = faceNormal * -1 230 | FaceArea = tool.extrude(faceNormal * -unfoldLength ) 231 | BendSolidFace = BalanceFace.common(FaceArea) 232 | #Part.show(FaceArea,"FaceArea") 233 | #Part.show(BendSolidFace,"BendSolidFace") 234 | #print([bendR, bendA, revAxisV, revAxisP, normal, flipped, BendSolidFace.Faces[0].normalAt(0,0)]) 235 | 236 | bendsolid = SheetMetalBendSolid.BendSolid(BendSolidFace.Faces[0], BendEdge, bendR, thk, neutralRadius, revAxisV, flipped) 237 | #Part.show(bendsolid,"bendsolid") 238 | solidlist.append(bendsolid) 239 | 240 | if flipped == True: 241 | bendA = -bendA 242 | if not(SolidFace.Faces): 243 | revAxisV = revAxisV * -1 244 | sketch_face = BalanceFace.cut(BendSolidFace) 245 | sketch_face.translate(faceNormal * unfoldLength) 246 | #Part.show(sketch_face,"sketch_face") 247 | sketch_face.rotate(revAxisP, -revAxisV, bendA) 248 | #Part.show(sketch_face,"Rsketch_face") 249 | TopFace = smCutFace(sketch_face, resultSolid) 250 | #Part.show(TopFace,"TopFace") 251 | 252 | #To get top face normal, flatsolid 253 | normal = TopFace.normalAt(0,0) 254 | Flatface = sketch_face.common(TopFace) 255 | BalanceFace = sketch_face.cut(Flatface) 256 | #Part.show(BalanceFace,"BalanceFace") 257 | Flatsolid = Flatface.extrude(normal * -thk) 258 | #Part.show(Flatsolid,"Flatsolid") 259 | solidlist.append(Flatsolid) 260 | 261 | #To get relief Solid fused 262 | if len(solidlist) > 1 : 263 | SMSolid = solidlist[0].multiFuse(solidlist[1:]) 264 | #Part.show(SMSolid,"SMSolid") 265 | SMSolid = SMSolid.removeSplitter() 266 | else : 267 | SMSolid = solidlist[0] 268 | #Part.show(SMSolid,"SMSolid") 269 | resultSolid = resultSolid.cut(SMSolid) 270 | 271 | Gui.ActiveDocument.getObject(MainObject.Name).Visibility = False 272 | Gui.ActiveDocument.getObject(sketch.Name).Visibility = False 273 | return resultSolid 274 | 275 | class SMSketchOnSheet: 276 | def __init__(self, obj): 277 | '''"Add Sketch based cut On Sheet metal" ''' 278 | selobj = Gui.Selection.getSelectionEx() 279 | 280 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Base Object") 281 | obj.addProperty("App::PropertyLinkSub", "baseObject", "Parameters",_tip_).baseObject = (selobj[0].Object, selobj[0].SubElementNames) 282 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Sketch on Sheetmetal") 283 | obj.addProperty("App::PropertyLink","Sketch","Parameters",_tip_).Sketch = selobj[1].Object 284 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Gap from Left Side") 285 | obj.addProperty("App::PropertyFloatConstraint","kfactor","Parameters",_tip_).kfactor = (0.5,0.0,1.0,0.01) 286 | obj.Proxy = self 287 | 288 | def execute(self, fp): 289 | '''"Print a short message when doing a recomputation, this method is mandatory" ''' 290 | 291 | s = smSketchOnSheetMetal(kfactor = fp.kfactor, sketch = fp.Sketch, selFaceNames = fp.baseObject[1], MainObject = fp.baseObject[0]) 292 | fp.Shape = s 293 | 294 | class SMSketchOnSheetVP: 295 | "A View provider that nests children objects under the created one" 296 | 297 | def __init__(self, obj): 298 | obj.Proxy = self 299 | self.Object = obj.Object 300 | 301 | def attach(self, obj): 302 | self.Object = obj.Object 303 | return 304 | 305 | def updateData(self, fp, prop): 306 | return 307 | 308 | def getDisplayModes(self,obj): 309 | modes=[] 310 | return modes 311 | 312 | def setDisplayMode(self,mode): 313 | return mode 314 | 315 | def onChanged(self, vp, prop): 316 | return 317 | 318 | def __getstate__(self): 319 | # return {'ObjectName' : self.Object.Name} 320 | return None 321 | 322 | def __setstate__(self,state): 323 | if state is not None: 324 | import FreeCAD 325 | doc = FreeCAD.ActiveDocument #crap 326 | self.Object = doc.getObject(state['ObjectName']) 327 | 328 | def claimChildren(self): 329 | objs = [] 330 | if hasattr(self.Object,"baseObject"): 331 | objs.append(self.Object.baseObject[0]) 332 | objs.append(self.Object.Sketch) 333 | return objs 334 | 335 | def getIcon(self): 336 | return os.path.join( iconPath , 'SheetMetal_SketchOnSheet.svg') 337 | 338 | class SMSketchOnSheetPDVP: 339 | "A View provider that nests children objects under the created one" 340 | 341 | def __init__(self, obj): 342 | obj.Proxy = self 343 | self.Object = obj.Object 344 | 345 | def attach(self, obj): 346 | self.Object = obj.Object 347 | return 348 | 349 | def updateData(self, fp, prop): 350 | return 351 | 352 | def getDisplayModes(self,obj): 353 | modes=[] 354 | return modes 355 | 356 | def setDisplayMode(self,mode): 357 | return mode 358 | 359 | def onChanged(self, vp, prop): 360 | return 361 | 362 | def __getstate__(self): 363 | # return {'ObjectName' : self.Object.Name} 364 | return None 365 | 366 | def __setstate__(self,state): 367 | if state is not None: 368 | import FreeCAD 369 | doc = FreeCAD.ActiveDocument #crap 370 | self.Object = doc.getObject(state['ObjectName']) 371 | 372 | def claimChildren(self): 373 | objs = [] 374 | if hasattr(self.Object,"Sketch"): 375 | objs.append(self.Object.Sketch) 376 | return objs 377 | 378 | def getIcon(self): 379 | return os.path.join( iconPath , 'SheetMetal_SketchOnSheet.svg') 380 | 381 | class AddSketchOnSheetCommandClass(): 382 | """Add Sketch On Sheet metal command""" 383 | 384 | def GetResources(self): 385 | return {'Pixmap' : os.path.join( iconPath , 'SheetMetal_SketchOnSheet.svg'), # the name of a svg file available in the resources 386 | 'MenuText': QtCore.QT_TRANSLATE_NOOP('SheetMetal','Sketch On Sheet metal'), 387 | 'Accel': "M, S", 388 | 'ToolTip' : QtCore.QT_TRANSLATE_NOOP('SheetMetal',' Extruded cut from Sketch On Sheet metal faces\n' 389 | '1. Select a flat face on sheet metal and\n' 390 | '2. Select a sketch on same face to create sheetmetal extruded cut.\n' 391 | '3. Use Property editor to modify other parameters')} 392 | 393 | def Activated(self): 394 | doc = FreeCAD.ActiveDocument 395 | view = Gui.ActiveDocument.ActiveView 396 | activeBody = None 397 | selobj = Gui.Selection.getSelectionEx()[0].Object 398 | if hasattr(view,'getActiveObject'): 399 | activeBody = view.getActiveObject('pdbody') 400 | if not smIsOperationLegal(activeBody, selobj): 401 | return 402 | doc.openTransaction("SketchOnSheet") 403 | if activeBody is None or not smIsPartDesign(selobj): 404 | a = doc.addObject("Part::FeaturePython","SketchOnSheet") 405 | SMSketchOnSheet(a) 406 | SMSketchOnSheetVP(a.ViewObject) 407 | else: 408 | #FreeCAD.Console.PrintLog("found active body: " + activeBody.Name) 409 | a = doc.addObject("PartDesign::FeaturePython","SketchOnSheet") 410 | SMSketchOnSheet(a) 411 | SMSketchOnSheetPDVP(a.ViewObject) 412 | activeBody.addObject(a) 413 | doc.recompute() 414 | doc.commitTransaction() 415 | return 416 | 417 | def IsActive(self): 418 | if len(Gui.Selection.getSelection()) < 2 : 419 | return False 420 | # selobj = Gui.Selection.getSelection()[1] 421 | # if str(type(selobj)) != "" : 422 | # return False 423 | return True 424 | 425 | Gui.addCommand('SMSketchOnSheet',AddSketchOnSheetCommandClass()) 426 | -------------------------------------------------------------------------------- /SheetMetalFormingCmd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ################################################################################### 3 | # 4 | # SheetMetalCmd.py 5 | # 6 | # Copyright 2015 Shai Seger 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | ################################################################################### 25 | 26 | from FreeCAD import Gui 27 | from PySide import QtCore, QtGui 28 | 29 | import FreeCAD, FreeCADGui, Part, os, math 30 | __dir__ = os.path.dirname(__file__) 31 | iconPath = os.path.join( __dir__, 'Resources', 'icons' ) 32 | smEpsilon = 0.0000001 33 | 34 | def smWarnDialog(msg): 35 | diag = QtGui.QMessageBox(QtGui.QMessageBox.Warning, 'Error in macro MessageBox', msg) 36 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 37 | diag.exec_() 38 | 39 | def smBelongToBody(item, body): 40 | if (body is None): 41 | return False 42 | for obj in body.Group: 43 | if obj.Name == item.Name: 44 | return True 45 | return False 46 | 47 | def smIsPartDesign(obj): 48 | return str(obj).find(" smEpsilon : 73 | break 74 | return thk 75 | 76 | def angleBetween(ve1, ve2): 77 | # Find angle between two vectors in degrees 78 | return math.degrees(ve1.getAngle(ve2)) 79 | 80 | def face_direction(face): 81 | yL = face.CenterOfMass 82 | uv = face.Surface.parameter(yL) 83 | nv = face.normalAt(uv[0], uv[1]) 84 | direction = yL.sub(nv + yL) 85 | #print([direction, yL]) 86 | return direction, yL 87 | 88 | def transform_tool(tool, base_face, tool_face, point = FreeCAD.Vector(0, 0, 0), angle = 0.0): 89 | # Find normal of faces & center to align faces 90 | direction1,yL1 = face_direction(base_face) 91 | direction2,yL2 = face_direction(tool_face) 92 | 93 | # Find angle between faces, axis of rotation & center of axis 94 | rot_angle = angleBetween(direction1, direction2) 95 | rot_axis = direction1.cross(direction2) 96 | if rot_axis == FreeCAD.Vector (0.0, 0.0, 0.0): 97 | rot_axis = FreeCAD.Vector(0, 1, 0).cross(direction2) 98 | rot_center = yL2 99 | #print([rot_center, rot_axis, rot_angle]) 100 | tool.rotate(rot_center, rot_axis, -rot_angle) 101 | tool.translate(-yL2 + yL1) 102 | #Part.show(tool, "tool") 103 | 104 | tool.rotate(yL1, direction1, angle) 105 | tool.translate(point) 106 | #Part.show(tool,"tool") 107 | return tool 108 | 109 | def makeforming(tool, base, base_face, thk, tool_faces = None, point = FreeCAD.Vector(0, 0, 0), angle = 0.0) : 110 | ## faces = [ face for face in tool.Shape.Faces for tool_face in tool_faces if not(face.isSame(tool_face)) ] 111 | # faces = [ face for face in tool.Shape.Faces if not face in tool_faces ] 112 | # tool_shell = Part.makeShell(faces) 113 | # offsetshell = tool_shell.makeOffsetShape(thk, 0.0, inter = False, self_inter = False, offsetMode = 0, join = 2, fill = True) 114 | offsetshell = tool.makeThickness(tool_faces, thk, 0.0001, False, False, 0, 0) 115 | cutSolid = tool.fuse(offsetshell) 116 | offsetshell_tran = transform_tool(offsetshell, base_face, tool_faces[0], point, angle) 117 | #Part.show(offsetshell1, "offsetshell1") 118 | cutSolid_trans = transform_tool(cutSolid, base_face, tool_faces[0], point, angle) 119 | base = base.cut(cutSolid_trans) 120 | base = base.fuse(offsetshell_tran) 121 | #base.removeSplitter() 122 | #Part.show(base, "base") 123 | return base 124 | 125 | class SMBendWall: 126 | def __init__(self, obj): 127 | '''"Add Forming Wall" ''' 128 | selobj = Gui.Selection.getSelectionEx() 129 | 130 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Offset from Center of Face") 131 | obj.addProperty("App::PropertyVectorDistance","offset","Parameters",_tip_) 132 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Suppress Forming Feature") 133 | obj.addProperty("App::PropertyBool","SuppressFeature","Parameters",_tip_).SuppressFeature = False 134 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Tool Position Angle") 135 | obj.addProperty("App::PropertyAngle","angle","Parameters",_tip_).angle = 0.0 136 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Thickness of Sheetmetal") 137 | obj.addProperty("App::PropertyDistance","thickness","Parameters",_tip_) 138 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Base Object") 139 | obj.addProperty("App::PropertyLinkSub", "baseObject", "Parameters",_tip_).baseObject = (selobj[0].Object, selobj[0].SubElementNames) 140 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Forming Tool Object") 141 | obj.addProperty("App::PropertyLinkSub", "toolObject", "Parameters",_tip_).toolObject = (selobj[1].Object, selobj[1].SubElementNames) 142 | _tip_ = QtCore.QT_TRANSLATE_NOOP("App::Property","Point Sketch on Sheetmetal") 143 | obj.addProperty("App::PropertyLink","Sketch","Parameters1",_tip_) 144 | obj.Proxy = self 145 | 146 | def execute(self, fp): 147 | '''"Print a short message when doing a recomputation, this method is mandatory" ''' 148 | 149 | base = fp.baseObject[0].Shape 150 | base_face = base.getElement(fp.baseObject[1][0]) 151 | thk = smthk(base, base_face) 152 | fp.thickness = thk 153 | tool = fp.toolObject[0].Shape 154 | tool_faces = [tool.getElement(fp.toolObject[1][i]) for i in range(len(fp.toolObject[1]))] 155 | 156 | offsetlist = [] 157 | if fp.Sketch: 158 | sketch = fp.Sketch.Shape 159 | for e in sketch.Edges: 160 | #print(type(e.Curve)) 161 | if isinstance(e.Curve, (Part.Circle, Part.ArcOfCircle)): 162 | pt1 = base_face.CenterOfMass 163 | pt2 = e.Curve.Center 164 | offsetPoint = pt2 - pt1 165 | #print(offsetPoint) 166 | offsetlist.append(offsetPoint) 167 | else: 168 | offsetlist.append(fp.offset) 169 | 170 | if not(fp.SuppressFeature) : 171 | for i in range(len(offsetlist)): 172 | a = makeforming(tool, base, base_face, thk, tool_faces, offsetlist[i], fp.angle.Value) 173 | base = a 174 | else : 175 | a = base 176 | fp.Shape = a 177 | Gui.ActiveDocument.getObject(fp.baseObject[0].Name).Visibility = False 178 | Gui.ActiveDocument.getObject(fp.toolObject[0].Name).Visibility = False 179 | if fp.Sketch: 180 | Gui.ActiveDocument.getObject(fp.Sketch.Name).Visibility = False 181 | 182 | class SMFormingVP: 183 | "A View provider that nests children objects under the created one" 184 | 185 | def __init__(self, obj): 186 | obj.Proxy = self 187 | self.Object = obj.Object 188 | 189 | def attach(self, obj): 190 | self.Object = obj.Object 191 | return 192 | 193 | def setupContextMenu(self, viewObject, menu): 194 | action = menu.addAction(FreeCAD.Qt.translate("QObject", "Edit %1").replace("%1", viewObject.Object.Label)) 195 | action.triggered.connect(lambda: self.startDefaultEditMode(viewObject)) 196 | return False 197 | 198 | def startDefaultEditMode(self, viewObject): 199 | document = viewObject.Document.Document 200 | if not document.HasPendingTransaction: 201 | text = FreeCAD.Qt.translate("QObject", "Edit %1").replace("%1", viewObject.Object.Label) 202 | document.openTransaction(text) 203 | viewObject.Document.setEdit(viewObject.Object, 0) 204 | 205 | def updateData(self, fp, prop): 206 | return 207 | 208 | def getDisplayModes(self,obj): 209 | modes=[] 210 | return modes 211 | 212 | def setDisplayMode(self,mode): 213 | return mode 214 | 215 | def onChanged(self, vp, prop): 216 | return 217 | 218 | def __getstate__(self): 219 | # return {'ObjectName' : self.Object.Name} 220 | return None 221 | 222 | def __setstate__(self,state): 223 | if state is not None: 224 | import FreeCAD 225 | doc = FreeCAD.ActiveDocument #crap 226 | self.Object = doc.getObject(state['ObjectName']) 227 | 228 | def claimChildren(self): 229 | objs = [] 230 | if hasattr(self.Object,"baseObject"): 231 | objs.append(self.Object.baseObject[0]) 232 | if hasattr(self.Object,"toolObject"): 233 | objs.append(self.Object.toolObject[0]) 234 | if hasattr(self.Object,"Sketch"): 235 | objs.append(self.Object.Sketch) 236 | return objs 237 | 238 | def getIcon(self): 239 | return os.path.join( iconPath , 'SheetMetal_Forming.svg') 240 | 241 | def setEdit(self,vobj,mode): 242 | taskd = SMFormingWallTaskPanel() 243 | taskd.obj = vobj.Object 244 | taskd.update() 245 | self.Object.ViewObject.Visibility=False 246 | self.Object.baseObject[0].ViewObject.Visibility=True 247 | self.Object.toolObject[0].ViewObject.Visibility=True 248 | FreeCADGui.Control.showDialog(taskd) 249 | return True 250 | 251 | def unsetEdit(self,vobj,mode): 252 | FreeCADGui.Control.closeDialog() 253 | self.Object.baseObject[0].ViewObject.Visibility=False 254 | self.Object.toolObject[0].ViewObject.Visibility=False 255 | self.Object.ViewObject.Visibility=True 256 | return False 257 | 258 | class SMFormingPDVP: 259 | "A View provider that nests children objects under the created one" 260 | 261 | def __init__(self, obj): 262 | obj.Proxy = self 263 | self.Object = obj.Object 264 | 265 | def attach(self, obj): 266 | self.Object = obj.Object 267 | return 268 | 269 | def updateData(self, fp, prop): 270 | return 271 | 272 | def getDisplayModes(self,obj): 273 | modes=[] 274 | return modes 275 | 276 | def setDisplayMode(self,mode): 277 | return mode 278 | 279 | def onChanged(self, vp, prop): 280 | return 281 | 282 | def __getstate__(self): 283 | # return {'ObjectName' : self.Object.Name} 284 | return None 285 | 286 | def __setstate__(self,state): 287 | if state is not None: 288 | import FreeCAD 289 | doc = FreeCAD.ActiveDocument #crap 290 | self.Object = doc.getObject(state['ObjectName']) 291 | 292 | def claimChildren(self): 293 | objs = [] 294 | if hasattr(self.Object,"toolObject"): 295 | objs.append(self.Object.toolObject[0]) 296 | if hasattr(self.Object,"Sketch"): 297 | objs.append(self.Object.Sketch) 298 | return objs 299 | 300 | def getIcon(self): 301 | return os.path.join( iconPath , 'SheetMetal_Forming.svg') 302 | 303 | def setEdit(self,vobj,mode): 304 | taskd = SMFormingWallTaskPanel() 305 | taskd.obj = vobj.Object 306 | taskd.update() 307 | self.Object.ViewObject.Visibility=False 308 | self.Object.baseObject[0].ViewObject.Visibility=True 309 | self.Object.toolObject[0].ViewObject.Visibility=False 310 | FreeCADGui.Control.showDialog(taskd) 311 | return True 312 | 313 | def unsetEdit(self,vobj,mode): 314 | FreeCADGui.Control.closeDialog() 315 | self.Object.baseObject[0].ViewObject.Visibility=False 316 | self.Object.toolObject[0].ViewObject.Visibility=False 317 | self.Object.ViewObject.Visibility=True 318 | return False 319 | 320 | class SMFormingWallTaskPanel: 321 | '''A TaskPanel for the Sheetmetal''' 322 | def __init__(self): 323 | 324 | self.obj = None 325 | self.form = QtGui.QWidget() 326 | self.form.setObjectName("SMBendWallTaskPanel") 327 | self.form.setWindowTitle("Binded faces/edges list") 328 | self.grid = QtGui.QGridLayout(self.form) 329 | self.grid.setObjectName("grid") 330 | self.title = QtGui.QLabel(self.form) 331 | self.grid.addWidget(self.title, 0, 0, 1, 2) 332 | self.title.setText("Select new face(s)/Edge(s) and press Update") 333 | 334 | # tree 335 | self.tree = QtGui.QTreeWidget(self.form) 336 | self.grid.addWidget(self.tree, 1, 0, 1, 2) 337 | self.tree.setColumnCount(2) 338 | self.tree.setHeaderLabels(["Name","Subelement"]) 339 | 340 | # buttons 341 | self.addButton = QtGui.QPushButton(self.form) 342 | self.addButton.setObjectName("addButton") 343 | self.addButton.setIcon(QtGui.QIcon(os.path.join( iconPath , 'SheetMetal_Update.svg'))) 344 | self.grid.addWidget(self.addButton, 3, 0, 1, 2) 345 | 346 | QtCore.QObject.connect(self.addButton, QtCore.SIGNAL("clicked()"), self.updateElement) 347 | self.update() 348 | 349 | def isAllowedAlterSelection(self): 350 | return True 351 | 352 | def isAllowedAlterView(self): 353 | return True 354 | 355 | def getStandardButtons(self): 356 | return int(QtGui.QDialogButtonBox.Ok) 357 | 358 | def update(self): 359 | 'fills the treewidget' 360 | self.tree.clear() 361 | if self.obj: 362 | f = self.obj.baseObject 363 | if isinstance(f[1],list): 364 | for subf in f[1]: 365 | #FreeCAD.Console.PrintLog("item: " + subf + "\n") 366 | item = QtGui.QTreeWidgetItem(self.tree) 367 | item.setText(0,f[0].Name) 368 | item.setIcon(0,QtGui.QIcon(":/icons/Tree_Part.svg")) 369 | item.setText(1,subf) 370 | else: 371 | item = QtGui.QTreeWidgetItem(self.tree) 372 | item.setText(0,f[0].Name) 373 | item.setIcon(0,QtGui.QIcon(":/icons/Tree_Part.svg")) 374 | item.setText(1,f[1][0]) 375 | 376 | f = self.obj.toolObject 377 | if isinstance(f[1],list): 378 | for subf in f[1]: 379 | #FreeCAD.Console.PrintLog("item: " + subf + "\n") 380 | item = QtGui.QTreeWidgetItem(self.tree) 381 | item.setText(0,f[0].Name) 382 | item.setIcon(0,QtGui.QIcon(":/icons/Tree_Part.svg")) 383 | item.setText(1,subf) 384 | else: 385 | item = QtGui.QTreeWidgetItem(self.tree) 386 | item.setText(0,f[0].Name) 387 | item.setIcon(0,QtGui.QIcon(":/icons/Tree_Part.svg")) 388 | item.setText(1,f[1][0]) 389 | self.retranslateUi(self.form) 390 | 391 | def updateElement(self): 392 | if self.obj: 393 | sel = FreeCADGui.Selection.getSelectionEx()[0] 394 | if sel.HasSubObjects: 395 | obj = sel.Object 396 | for elt in sel.SubElementNames: 397 | if "Face" in elt: 398 | face = self.obj.baseObject 399 | found = False 400 | if (face[0] == obj.Name): 401 | if isinstance(face[1],tuple): 402 | for subf in face[1]: 403 | if subf == elt: 404 | found = True 405 | else: 406 | if (face[1][0] == elt): 407 | found = True 408 | if not found: 409 | self.obj.baseObject = (sel.Object, sel.SubElementNames) 410 | 411 | sel = FreeCADGui.Selection.getSelectionEx()[1] 412 | if sel.HasSubObjects: 413 | obj = sel.Object 414 | for elt in sel.SubElementNames: 415 | if "Face" in elt: 416 | face = self.obj.toolObject 417 | found = False 418 | if (face[0] == obj.Name): 419 | if isinstance(face[1],tuple): 420 | for subf in face[1]: 421 | if subf == elt: 422 | found = True 423 | else: 424 | if (face[1][0] == elt): 425 | found = True 426 | if not found: 427 | self.obj.toolObject = (sel.Object, sel.SubElementNames) 428 | self.update() 429 | 430 | def accept(self): 431 | FreeCAD.ActiveDocument.recompute() 432 | FreeCADGui.ActiveDocument.resetEdit() 433 | #self.obj.ViewObject.Visibility=True 434 | return True 435 | 436 | def retranslateUi(self, TaskPanel): 437 | #TaskPanel.setWindowTitle(QtGui.QApplication.translate("draft", "Faces", None)) 438 | self.addButton.setText(QtGui.QApplication.translate("draft", "Update", None)) 439 | 440 | 441 | class AddFormingWallCommand(): 442 | """Add Forming Wall command""" 443 | 444 | def GetResources(self): 445 | return {'Pixmap' : os.path.join( iconPath , 'SheetMetal_Forming.svg') , # the name of a svg file available in the resources 446 | 'MenuText': QtCore.QT_TRANSLATE_NOOP('SheetMetal','Make Forming in Wall') , 447 | 'Accel': "M, F", 448 | 'ToolTip' : QtCore.QT_TRANSLATE_NOOP('SheetMetal','Make a forming using tool in metal sheet\n' 449 | '1. Select a flat face on sheet metal and\n' 450 | '2. Select face(s) on forming tool Shape to create Formed sheetmetal.\n' 451 | '3. Use Suppress in Property editor to disable during unfolding\n' 452 | '4. Use Property editor to modify other parameters')} 453 | 454 | def Activated(self): 455 | doc = FreeCAD.ActiveDocument 456 | view = Gui.ActiveDocument.ActiveView 457 | activeBody = None 458 | selobj = Gui.Selection.getSelectionEx()[0].Object 459 | if hasattr(view,'getActiveObject'): 460 | activeBody = view.getActiveObject('pdbody') 461 | if not smIsOperationLegal(activeBody, selobj): 462 | return 463 | doc.openTransaction("WallForming") 464 | if activeBody is None or not smIsPartDesign(selobj): 465 | a = doc.addObject("Part::FeaturePython","WallForming") 466 | SMBendWall(a) 467 | SMFormingVP(a.ViewObject) 468 | else: 469 | #FreeCAD.Console.PrintLog("found active body: " + activeBody.Name) 470 | a = doc.addObject("PartDesign::FeaturePython","WallForming") 471 | SMBendWall(a) 472 | SMFormingPDVP(a.ViewObject) 473 | activeBody.addObject(a) 474 | doc.recompute() 475 | doc.commitTransaction() 476 | return 477 | 478 | def IsActive(self): 479 | if len(Gui.Selection.getSelection()) < 2 or len(Gui.Selection.getSelectionEx()[0].SubElementNames) < 1: 480 | return False 481 | selobj = Gui.Selection.getSelection()[0] 482 | if str(type(selobj)) == "": 483 | return False 484 | for selFace in Gui.Selection.getSelectionEx()[0].SubObjects: 485 | if type(selFace) != Part.Face : 486 | return False 487 | return True 488 | 489 | Gui.addCommand('SMFormingWall', AddFormingWallCommand()) 490 | 491 | --------------------------------------------------------------------------------