├── .gitattributes ├── .gitignore ├── CheckForPlacements.FCMacro ├── FixLineWidth.FCMacro ├── LICENSE ├── README.md ├── TDChainDistances.FCMacro ├── TDCustomFormat.FCMacro ├── TDEdgeIntersection.FCMacro ├── TDTangentialMeasure.FCMacro ├── TDThread.FCMacro ├── Voronoi.FCMacro └── icons ├── CustomFormat.svg ├── EdgeIntersection.svg ├── TDChainDistances.svg ├── TDTangentialMeasure.svg └── Voronoi.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | *.FCMacro linguist-language=Python 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /CheckForPlacements.FCMacro: -------------------------------------------------------------------------------- 1 | print("Checking replacing all placements with default.") 2 | print("===============================================") 3 | for b in App.activeDocument().findObjects("PartDesign::Body"): 4 | if not b.Placement.isNull(): 5 | print("!!! Body {} has a non null placement --> resetting!".format(b.Label)) 6 | 7 | b.Placement.Base = FreeCAD.Base.Vector() 8 | b.Placement.Rotation = FreeCAD.Base.Rotation() 9 | else: 10 | print("Body {} looks fine...".format(b.Label)) 11 | print("done...") -------------------------------------------------------------------------------- /FixLineWidth.FCMacro: -------------------------------------------------------------------------------- 1 | for v in App.activeDocument().findObjects("TechDraw::DrawViewDimension"): 2 | # v.LineWidth = 0.25 3 | # The precision of %g is how large the value should be, unless it is printed as exponent. 4 | # Maybe we need to adjust this later... 5 | v.FormatSpec = u"%.5g" 6 | # v.Fontsize = 3.5 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sebastian Bachmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeCAD_macros 2 | My and collected macros for FreeCAD - directly from ~/.FreeCAD/Macro 3 | 4 | Some macros are not written by me, but copied from other sources. Please inspect the licence of those files individually! 5 | 6 | Most of my macros are designed to work with a Python3/QT5 build of FreeCAD! 7 | 8 | **ATTENTION**: due to [changes in TechDraw](https://github.com/FreeCAD/FreeCAD/commit/f2f7d22b8e), some of the TechDraw related macros will only work in a recent Version **>=0.19.20943**! 9 | 10 | # Add the Macros to your toolbar: 11 | 12 | Follow the instructions in the FreeCAD wiki regaring [Toolbar Customization](https://wiki.freecadweb.org/Customize_Toolbars). 13 | Do not forget to add the `icons` folder in order to use the provided icons for 14 | the macros. 15 | 16 | ## TDCustomFormat.FCMacro 17 | 18 | This macro is intended to be used in the TechDraw Workbench. 19 | It allows you to set the FormatSpec of a dimension enhanced with some common CAD 20 | symbols. 21 | 22 | To use the macro, select one or more dimensions you want to change the 23 | FormatSpec. 24 | Execute the macro and click on the symbols you want to enhance the format string 25 | with. Symbols are inserted at the current cursor position, which is the 26 | beginning by default. 27 | Then click OK to set the format string. 28 | 29 | Note on the `g` format string: In my opinion the `g` format has an advantage 30 | over `f` as it removes trailing zeros from the string. One disadvantage is, that 31 | you can only specify the total number of decimal places and that it will switch 32 | to exponential format if the value is too large. 33 | 34 | 35 | ## TDChainDistances.FCMacro 36 | 37 | With this macro you can properly align dimensions to be used as chain measures. 38 | It also aligns the dimension value to the center of the line, if it was moved 39 | before. 40 | 41 | Select at least one dimension line which shall be aligned. 42 | Only DistanceX and DistanceY are supported. 43 | 44 | 45 | ## TDEdgeIntersection.FCMacro 46 | 47 | This macro is intended to be used in the TechDraw Workbench. 48 | 49 | Select two Edges of a DrawViewPart to compute the intersection point. 50 | A cosmetic vertex is created at the point of intersection. 51 | 52 | You will get an error message if: 53 | 54 | * a different number than two edges is selected 55 | * the lines are parallel 56 | 57 | 58 | ## TDTangentialMeasure.FCMacro 59 | 60 | This macro is intended to be used in the TechDraw Workbench. 61 | 62 | The idea is to be able to create dimensions between tangents of two arcs. 63 | A third axis has to be chosen as the perpendicular axis for the tangents. 64 | 65 | As TechDraw can not create dimensions along an arbitrary axis (yet), the macro creates four cosmetic 66 | vertices in order to use the normal distance dimension tool. 67 | 68 | 69 | 70 | # Licence 71 | 72 | All my macros are released under MIT Licence. 73 | 74 | 75 | Work based on other projects: 76 | 77 | * `EdgeIntersection.svg`: Based on [techdraw-2linecenterline.svg](https://github.com/FreeCAD/FreeCAD/blob/941968b37cd45505a5668a1df17ba9b8d6f9a66b/src/Mod/TechDraw/Gui/Resources/icons/actions/techdraw-2linecenterline.svg) 78 | * `TDChainDistances.svg`: Based on [techdraw_dimension_horizontal.svg](https://github.com/FreeCAD/FreeCAD/blob/291bad6cba925cb2a69033ce0d9f748814348398/src/Mod/TechDraw/Gui/Resources/icons/TechDraw_Dimension_Horizontal.svg) 79 | -------------------------------------------------------------------------------- /TDChainDistances.FCMacro: -------------------------------------------------------------------------------- 1 | """ 2 | This macro creates chain measurements, as it aligns all distances to the same coordinate 3 | It also re-centers the distance value. 4 | If only a single distance measure is given, it will only be re-centered. 5 | 6 | The first dimension selected will be the reference. 7 | All other dimension must be of the same type! 8 | The dimensions must be for parallel edges, otherwise the macro wont work. 9 | """ 10 | 11 | __Name__ = 'TDChainDistances' 12 | __Comment__ = 'Create Chain Distances in TechDraw' 13 | __Author__ = 'Sebastian Bachmann' 14 | __Version__ = '1.0' 15 | __Date__ = '2019-09-16' 16 | __License__ = 'MIT' 17 | __Web__ = 'https://github.com/reox/FreeCAD_Macros' 18 | __Wiki__ = '' 19 | __Icon__ = 'icons/TDChainDistances.svg' 20 | __Help__ = 'Select any number of distances and use the macro. ' \ 21 | 'The distances must be parallel. ' \ 22 | 'The first distance is taken as reference.' 23 | __Status__ = 'Stable' 24 | __Requires__ = '>=0.19.18234; py3 only' 25 | __Communication__ = 'https://github.com/reox/FreeCAD_Macros/issues' 26 | 27 | # Copyright (c) 2019, Sebastian Bachmann 28 | # 29 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 30 | # this software and associated documentation files (the "Software"), to deal in 31 | # the Software without restriction, including without limitation the rights to 32 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 33 | # the Software, and to permit persons to whom the Software is furnished to do so, 34 | # subject to the following conditions: 35 | # 36 | # The above copyright notice and this permission notice shall be included in all 37 | # copies or substantial portions of the Software. 38 | # 39 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 41 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 42 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 43 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 44 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 45 | 46 | import math 47 | 48 | from PySide.QtGui import QMessageBox 49 | from PySide import QtCore 50 | 51 | 52 | def msg(title, message): 53 | diag = QMessageBox(QMessageBox.Warning, title, message) 54 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 55 | diag.exec_() 56 | 57 | 58 | def is_above(a, b, p): 59 | """Checks if Point p is above the line a-b""" 60 | # Extra check for division by zero -> vertical line 61 | if (b.x - a.x) == 0: 62 | return p.x < a.x 63 | print(p.y, (((b.y - a.y) / (b.x - a.x) * (p.x - a.x)) + a.y)) 64 | if p.y > (((b.y - a.y) / (b.x - a.x) * (p.x - a.x)) + a.y): 65 | return True 66 | return False 67 | 68 | 69 | def get_dim_vec(point): 70 | """ 71 | Returns the center of the line (m) which the dimension is measuring (a vector), 72 | the normal vector (v) on the line of measurement (a unit vector) 73 | and the orthogonal length to the dimension from the line. 74 | 75 | All vectors are returned in model space. 76 | """ 77 | 78 | #FIXME: somewhere in this function there is a bug for Dimensions, 79 | # that if one of the dimensions is close to the line itself, it will 80 | # flip around. Usually, the second click on this macro resolves the issue though 81 | 82 | 83 | # getLinearPoints() --> returns in model space 84 | # dimension .X and .Y --> return in paper space (not scaled, need to divide by scale) 85 | # makeCosmeticVertex --> requires scaled paper space 86 | 87 | # a, b are in model space 88 | a, b = point.getLinearPoints() 89 | # get the center vector of the line in question 90 | m = (a + b) / 2.0 91 | 92 | # Get the current location of the dimension as a vector 93 | # NOTE: has to be converted from paper space to model space by swapping Y axis 94 | D = FreeCAD.Vector(point.X, -point.Y, 0) 95 | 96 | # --> If you need to debug the current locations 97 | #if len(point.OutList) > 1: 98 | # print("Uhm, outlist has more entries than expected?!") 99 | #view = point.OutList[0] 100 | # plot the current location of the dimension 101 | #view.makeCosmeticVertex(FreeCAD.Vector(point.X, point.Y, 0) / view.Scale) 102 | # Plot the center of the line segment 103 | #view.makeCosmeticVertex(FreeCAD.Vector(m.x, -m.y, 0) / view.Scale) 104 | 105 | # get the line as a vector 106 | # We want that a is the vector left from b 107 | # As the sorting in the list might be different, we need to check that. 108 | if a.x < b.x: 109 | # Vector pointing from b to a, i.e. if a is left from b, the vector points 110 | # in the negative direction 111 | l = a - b 112 | else: 113 | if a.y < b.y: 114 | # There might be the case that the line is straight up or down 115 | l = a - b 116 | else: 117 | l = b - a 118 | if l.x == 0 or point.Type == 'DistanceY': 119 | # This is rather a DistanceY 120 | if D.x < m.x: 121 | return m, FreeCAD.Vector(-1, 0, 0), m.x - D.x 122 | return m, FreeCAD.Vector(1, 0, 0), D.x - m.x 123 | if l.y == 0 or point.Type == 'DistanceX': 124 | # This is rather a DistanceX 125 | if D.y < m.y: 126 | return m, FreeCAD.Vector(0, -1, 0), m.y - D.y 127 | return m, FreeCAD.Vector(0, 1, 0), D.y - m.y 128 | 129 | # If not horizontal or vertical lines; get orthogonal vector on l 130 | v = FreeCAD.Vector(-(l.x ** -1), (l.y ** -1), 0) 131 | v.normalize() # Normalize as l might be any orthogonal vector 132 | 133 | # We need to know if the dimension line is above or beyond the edge 134 | if is_above(a, b, D): 135 | v = -v 136 | 137 | # Now, we calculate the vector between the center Point and the current 138 | # location of the dimension 139 | x = m - D 140 | length = x.Length 141 | 142 | # We need the angle between the vectors to calculate the distance 143 | x.normalize() 144 | # The actual length of v is now the cos(alpha) times |x| 145 | length = math.fabs(x.dot(v)) * length 146 | # This length times v gives the new point for the dimension 147 | 148 | return m, v, length 149 | 150 | 151 | def main(): 152 | sel = FreeCADGui.Selection.getSelection() 153 | 154 | if not sel: 155 | # Do nothing, empty selection 156 | return 157 | 158 | if not all(map(lambda x: x.isDerivedFrom("TechDraw::DrawViewDimension"), sel)): 159 | msg("Error", "You need to only select DrawViewDimensions!") 160 | return 161 | 162 | reference = next(iter(sel)) 163 | 164 | if reference.Type not in ("DistanceX", "DistanceY", "Distance"): 165 | msg("Error", "The dimension must be DistanceX, DistanceY or Distance!") 166 | return 167 | 168 | if not all(map(lambda x: x.Type == reference.Type, sel)): 169 | msg("Error", "All dimensions must have the same type as the first selected") 170 | return 171 | 172 | if reference.Type == "Distance": 173 | ref_m, ref_v, ref_length = get_dim_vec(reference) 174 | # Make sure, all dimensions point into the same direction 175 | # Some dimensions might be under the line, hence their vector is 176 | # negative the reference 177 | for s in sel: 178 | _, check_v, _ = get_dim_vec(s) 179 | # TODO: check if 0.000001 is a reasonable number; time will tell 180 | if not ref_v.isEqual(check_v, 0.000001): 181 | if not ref_v.isEqual(-check_v, 0.000001): 182 | msg("Error", "{} point into different directions!".format(s.Name)) 183 | return 184 | else: 185 | ref_length = 0 186 | ref_v = None 187 | 188 | 189 | for s in sel: 190 | m, cur_v, _ = get_dim_vec(s) 191 | # Need to check if special cases, where Type==Distance is used for 192 | # horizontal or vertical lines 193 | if reference.Type == "DistanceY" or math.fabs(cur_v.x) == 1: 194 | s.X = reference.X 195 | s.Y = -m.y 196 | elif reference.Type == "DistanceX" or math.fabs(cur_v.y) == 1: 197 | s.Y = reference.Y 198 | s.X = m.x 199 | elif reference.Type == "Distance": 200 | # For each line, must calculate the center and normal 201 | # But the lines might be anywhere, hence we need to calculate the 202 | # distance in direction of the normal vector between the reference 203 | # and the current one. 204 | x = m - ref_m 205 | v = ref_v * (ref_length - (math.cos(x.getAngle(cur_v)) * x.Length)) 206 | 207 | s.X = m.x + v.x 208 | s.Y = -(m.y + v.y) 209 | 210 | 211 | if __name__ == "__main__": 212 | main() 213 | -------------------------------------------------------------------------------- /TDCustomFormat.FCMacro: -------------------------------------------------------------------------------- 1 | """ 2 | Custom Format Macro for the TechDraw Workbench 3 | 4 | Select dimensions and click the macro in order to customize the format shown. 5 | 6 | The Macro includes the most common (and available as Unicode!) characters 7 | used in technical drawings. 8 | """ 9 | __Name__ = 'TDCustomFormat' 10 | __Comment__ = 'Change the format of dimensions' 11 | __Author__ = 'Sebastian Bachmann' 12 | __Version__ = '1.0' 13 | __Date__ = '2020-05-07' 14 | __License__ = 'MIT' 15 | __Web__ = 'https://github.com/reox/FreeCAD_Macros' 16 | __Wiki__ = '' 17 | __Icon__ = 'icons/CustomFormat.svg' 18 | __Help__ = 'Select dimension(s) and run the macro' 19 | __Status__ = 'Stable' 20 | __Requires__ = '>=0.19.18234; py3 only' 21 | __Communication__ = 'https://github.com/reox/FreeCAD_Macros/issues' 22 | 23 | # Copyright (c) 2019, Sebastian Bachmann 24 | # 25 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 26 | # this software and associated documentation files (the "Software"), to deal in 27 | # the Software without restriction, including without limitation the rights to 28 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 29 | # the Software, and to permit persons to whom the Software is furnished to do so, 30 | # subject to the following conditions: 31 | # 32 | # The above copyright notice and this permission notice shall be included in all 33 | # copies or substantial portions of the Software. 34 | # 35 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 37 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 38 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 39 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 40 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | 42 | # Is a shim provided by FreeCAD 43 | from PySide.QtGui import ( 44 | QMainWindow, 45 | QDialog, 46 | QLineEdit, 47 | QPushButton, 48 | QGridLayout, 49 | QLabel, 50 | QGroupBox, 51 | QSpacerItem, 52 | ) 53 | 54 | 55 | class Form(QDialog): 56 | _defaultformat = "%.5g" 57 | 58 | # See also https://en.wikipedia.org/wiki/Geometric_dimensioning_and_tolerancing#Symbols 59 | # https://github.com/hikikomori82/osifont 60 | # https://de.wikipedia.org/wiki/Unicodeblock_Verschiedene_technische_Zeichen 61 | symbols = { 62 | 'GD&&T': { 63 | '\u23e4': 'Straightness', 64 | '\u23e5': 'Flatness', 65 | '\u25cb': 'Circularity', 66 | '\u232d': 'Cylindricity', 67 | '\u2225': 'Parallelism', 68 | '\u27c2': 'Perpendicularity', 69 | '\u2220': 'Angularity', 70 | '\u2312': 'Profile of a line', 71 | '\u2313': 'Profile of a surface', 72 | '\u2197': 'Circular runout', 73 | '\u2330': 'Total runout', 74 | '\u2316': 'Position', 75 | '\u25ce': 'Concentricity', 76 | '\u232f': 'Symmetry', 77 | }, 78 | 'Modifiers': { 79 | '\u24b6': 'derived geometry element', # A, not in osifont 80 | '\u24b8': 'Minimax (Tschebyschew)', # C, not in osifont 81 | '\u24ba': 'Hull condition', # E 82 | '\u24bb': 'Free state', # F 83 | '\u24bc': 'Least square geomtry element', # G, not in osifont 84 | '\u24c1': 'Least material condition (LMC)', # L 85 | '\u24c2': 'Maximum material condition (MMC)', # M 86 | '\u24c3': 'least inscribed geometry element', # N, not in osifont 87 | '\u24c5': 'Projected tolerance zone', # P 88 | '\u24c7': 'Reciprocity condition', # R, not in osifont 89 | '\u24c8': 'Regardless of feature size (RFS)', # S 90 | '\u24c9': 'Tangent plane', # T 91 | '\u24ca': 'Unequal Bilateral', # U 92 | '\u24cd': 'most inscribed geomtry element', # X, not in osifont 93 | }, 94 | 'Radius && Diameter': { 95 | 'R': 'Radius', 96 | '\u2300': 'Diameter', 97 | 'SR': 'Radius of sphere', 98 | 'S\u2300': 'Diameter of sphere', 99 | '\u25a1': 'Square', 100 | }, 101 | 'Angles': { 102 | '\u00b0': 'Degree', 103 | '\u2032': '(Arc) Minute', 104 | '\u2033': '(Arc) Second', 105 | '\u2034': '(Arc) Tertie', 106 | }, 107 | 'Other': { 108 | '\u2332': 'Taper', 109 | '\u2333': 'Slope', 110 | '\u2334': 'Counterbore', 111 | '\u2335': 'Countersink', 112 | '\u00b1': 'Plus - Minus', 113 | '\u2104': 'Centerline', 114 | '\u2194': 'Left/right arrow', 115 | '\u21a7': 'Downward arrow', 116 | '\u00d7': 'Multiplication sign', 117 | }, 118 | 'Greek Letters': { 119 | '\u0394': 'Capital delta', 120 | '\u03a3': 'Capital sigma', 121 | '\u03a9': 'Capital omega', 122 | '\u03bc': 'Small mu', 123 | '\u03c3': 'Small sigma', 124 | '\u03c6': 'Small phi', 125 | '\u2375': 'Small omega', 126 | 127 | } 128 | } 129 | previewnumber = 23.123456789 130 | 131 | stylesheet = 'font-family: osifont; font-size:24px;' 132 | 133 | columns = 5 134 | 135 | def __init__(self, parent=None): 136 | super(Form, self).__init__(parent) 137 | self.setWindowTitle("Format Symbols") 138 | 139 | layout = QGridLayout() 140 | 141 | for n, (grp, items) in enumerate(self.symbols.items()): 142 | box = QGroupBox(grp) 143 | local_layout = QGridLayout() 144 | 145 | box.setLayout(local_layout) 146 | for i, (sym, desc) in enumerate(items.items()): 147 | btn = QPushButton(sym) 148 | btn.setStyleSheet(self.stylesheet) 149 | btn.setToolTip(desc) 150 | btn.clicked.connect(self.addSymbol) 151 | btn.setMinimumHeight(36) 152 | local_layout.addWidget(btn, i // self.columns, i % self.columns) 153 | 154 | # Fill up row 155 | if i < self.columns: 156 | for x in range(i, self.columns): 157 | # FIXME 158 | # local_layout.addItem(QSpacerItem(btn.width(), btn.height()), x // self.columns, x % self.columns) 159 | pass 160 | 161 | layout.addWidget(box, n, 0, 1, self.columns) 162 | 163 | 164 | tot_lines = len(self.symbols) 165 | 166 | self.formatspec = QLineEdit(self.defaultformat) 167 | self.formatspec.textChanged.connect(self.preview) 168 | layout.addWidget(QLabel("Format:", self), tot_lines + 1, 0) 169 | layout.addWidget(self.formatspec, tot_lines + 1, 1, 1, 2) 170 | 171 | reset = QPushButton("Reset") 172 | reset.clicked.connect(self.reset) 173 | layout.addWidget(reset, tot_lines + 1, 3) 174 | 175 | layout.addWidget(QLabel("Preview:"), tot_lines + 2, 0) 176 | self.previewlabel = QLabel("") 177 | self.previewlabel.setStyleSheet(self.stylesheet) 178 | layout.addWidget(self.previewlabel, tot_lines + 2, 1) 179 | 180 | ok = QPushButton("OK") 181 | ok.clicked.connect(self.write) 182 | abort = QPushButton("Abort") 183 | abort.clicked.connect(self.exit) 184 | 185 | layout.addWidget(ok, tot_lines + 3, 2) 186 | layout.addWidget(abort, tot_lines + 3, 3) 187 | 188 | self.setLayout(layout) 189 | self.preview() 190 | # Move to the beginning, usually we want to insert here... 191 | self.formatspec.setCursorPosition(0) 192 | 193 | def addSymbol(self): 194 | self.formatspec.insert(self.sender().text()) 195 | 196 | def write(self): 197 | for sel in FreeCADGui.Selection.getSelection(): 198 | if hasattr(sel, "FormatSpec"): 199 | # assume it is a Dimension... 200 | sel.FormatSpec = self.formatspec.text() 201 | sel.recompute() 202 | elif hasattr(sel, "Text"): 203 | # Looks like a Balloon type feature 204 | sel.Text = self.formatspec.text() 205 | sel.recompute() 206 | self.close() 207 | 208 | @property 209 | def defaultformat(self): 210 | if len(FreeCADGui.Selection.getSelection()) != 1: 211 | return self._defaultformat 212 | 213 | sel = FreeCADGui.Selection.getSelection()[0] 214 | if hasattr(sel, "FormatSpec"): 215 | # assume it is a Dimension... 216 | return sel.FormatSpec 217 | elif hasattr(sel, "Text"): 218 | # Looks like a Balloon type feature 219 | return sel.Text 220 | 221 | return self._defaultformat 222 | 223 | def preview(self): 224 | self.previewlabel.setText(self.formatspec.text() % (self.previewnumber)) 225 | 226 | def reset(self): 227 | self.formatspec.setText(self.defaultformat) 228 | 229 | def exit(self, event): 230 | self.close() 231 | 232 | 233 | if __name__ == '__main__': 234 | form = Form() 235 | form.show() 236 | -------------------------------------------------------------------------------- /TDEdgeIntersection.FCMacro: -------------------------------------------------------------------------------- 1 | """ 2 | This Macro is intended to be used in the TechDraw Workbench, 3 | to find the intersection point of two Edges inside a DrawViewPart. 4 | 5 | Select two Egdes and run the Macro. The cosmetic vertex will be created 6 | at the intersection point. 7 | """ 8 | 9 | __Name__ = 'TDEdgeIntersection' 10 | __Comment__ = 'Inserts a cosmetic vertex at the position where two lines intersect' 11 | __Author__ = 'Sebastian Bachmann' 12 | __Version__ = '1.0' 13 | __Date__ = '2020-05-07' 14 | __License__ = 'MIT' 15 | __Web__ = 'https://github.com/reox/FreeCAD_Macros' 16 | __Wiki__ = '' 17 | __Icon__ = 'icons/EdgeIntersection.svg' 18 | __Help__ = 'Select two straight lines and run the macro' 19 | __Status__ = 'Stable' 20 | __Requires__ = '>=0.19.20943; py3 only' 21 | __Communication__ = 'https://github.com/reox/FreeCAD_Macros/issues' 22 | 23 | 24 | # Copyright (c) 2019, Sebastian Bachmann 25 | # 26 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 27 | # this software and associated documentation files (the "Software"), to deal in 28 | # the Software without restriction, including without limitation the rights to 29 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 30 | # the Software, and to permit persons to whom the Software is furnished to do so, 31 | # subject to the following conditions: 32 | # The above copyright notice and this permission notice shall be included in all 33 | # copies or substantial portions of the Software. 34 | # 35 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 37 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 38 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 39 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 40 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | 42 | 43 | import numpy as np 44 | from PySide.QtGui import QMessageBox 45 | from PySide import QtCore 46 | 47 | 48 | def msg(title, message): 49 | diag = QMessageBox(QMessageBox.Warning, title, message) 50 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 51 | diag.exec_() 52 | 53 | 54 | def find_intersection(): 55 | """ 56 | Finds the intersection point of two edges 57 | And creates a cosmetic vertex there 58 | """ 59 | # Get the current view: 60 | cur_view = Gui.Selection.getSelectionEx()[0].Object 61 | 62 | if not cur_view.isDerivedFrom("TechDraw::DrawViewPart"): 63 | msg("Wrong Selection", "Must select Edges of DrawViewPart!") 64 | return 65 | 66 | # Get the names of the selected edges: 67 | elem = Gui.Selection.getSelectionEx()[0].SubElementNames 68 | 69 | if not all(map(lambda x: x.startswith('Edge'), elem)) or len(elem) != 2: 70 | msg("Wrong Selection", "You have not selected two edges!") 71 | return 72 | 73 | try: 74 | indices = map(lambda x: int(x.replace('Edge', '')), elem) 75 | except ValueError as e: 76 | msg("Something is wrong", "Can not parse Edge indices: {}".format(e)) 77 | return 78 | 79 | (p, q), (r, s) = list(map(lambda i: (i[0].Point, i[1].Point), map(lambda a: 80 | cur_view.getEdgeByIndex(a).Vertexes, indices))) 81 | 82 | # Want to solve: p + t(q - p) = r + v(s - r) 83 | # t and v are costants, p,q,r,s are vectors 84 | a = np.array([[q.x - p.x, -s.x + r.x], [q.y - p.y, -s.y + r.y]]) 85 | b = np.array([r.x - p.x, r.y - p.y]) 86 | try: 87 | t, v = np.linalg.solve(a, b) 88 | except np.linalg.LinAlgError: 89 | # usually singular matrix -> det(a) = 0 -> parallel lines 90 | msg("Singular Matrix", "The lines are probably parallel, no intersection possible!") 91 | else: 92 | x = p.x + t * (q.x - p.x) 93 | y = r.y + v * (s.y - r.y) 94 | res = FreeCAD.Vector(x, y, 0) 95 | 96 | # Add the Vertex 97 | cur_view.makeCosmeticVertex(res) 98 | 99 | # Recompute 100 | App.activeDocument().recompute(None,True,True) 101 | 102 | 103 | if __name__ == "__main__": 104 | find_intersection() 105 | -------------------------------------------------------------------------------- /TDTangentialMeasure.FCMacro: -------------------------------------------------------------------------------- 1 | """ 2 | This Macro is intended to be used in the TechDraw Workbench, 3 | to find the tangential points on two circles, which are parallel to a given reference. 4 | 5 | Select three edges: two circles and a "axis" line. 6 | 7 | """ 8 | __Name__ = 'TDTangentialMeasure' 9 | __Comment__ = 'Create a tangential measure on two arcs by aligning to an axis' 10 | __Author__ = 'Sebastian Bachmann' 11 | __Version__ = '0.9' 12 | __Date__ = '2020-05-07' 13 | __License__ = 'MIT' 14 | __Web__ = 'https://github.com/reox/FreeCAD_Macros' 15 | __Wiki__ = '' 16 | __Icon__ = 'icons/TDTangentialMeasure.svg' 17 | __Help__ = 'Select two arcs where the tangential measure shall be applied ' \ 18 | 'and an additional straight line which will be the axis. ' \ 19 | 'The macro will create four cosmetic vertices: two at the arcs ' \ 20 | 'and two at the opposing sites in order to apply a distance measure.' 21 | __Status__ = 'Beta' 22 | __Requires__ = '>=0.19.20943; py3 only' 23 | __Communication__ = 'https://github.com/reox/FreeCAD_Macros/issues' 24 | 25 | # Copyright (c) 2020, Sebastian Bachmann 26 | # 27 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 28 | # this software and associated documentation files (the "Software"), to deal in 29 | # the Software without restriction, including without limitation the rights to 30 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 31 | # the Software, and to permit persons to whom the Software is furnished to do so, 32 | # subject to the following conditions: 33 | # The above copyright notice and this permission notice shall be included in all 34 | # copies or substantial portions of the Software. 35 | # 36 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 38 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 39 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 40 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 41 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | 43 | import numpy as np 44 | from PySide.QtGui import QMessageBox 45 | from PySide import QtCore 46 | 47 | 48 | def msg(title, message): 49 | diag = QMessageBox(QMessageBox.Warning, title, message) 50 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 51 | diag.exec_() 52 | 53 | 54 | def find_tangential(): 55 | """ 56 | Finds the intersection point of two edges 57 | And creates a cosmetic vertex there 58 | """ 59 | # Get the current view: 60 | cur_view = Gui.Selection.getSelectionEx()[0].Object 61 | 62 | 63 | if not cur_view.isDerivedFrom("TechDraw::DrawViewPart"): 64 | msg("Wrong Selection", "Must select Edges of DrawViewPart!") 65 | return 66 | 67 | # Get the names of the selected edges: 68 | elem = Gui.Selection.getSelectionEx()[0].SubElementNames 69 | 70 | if not all(map(lambda x: x.startswith('Edge'), elem)) or len(elem) != 3: 71 | msg("Wrong Selection", "You have not selected three edges!") 72 | return 73 | 74 | try: 75 | indices = map(lambda x: int(x.replace('Edge', '')), elem) 76 | except ValueError as e: 77 | msg("Something is wrong", "Can not parse Edge indices: {}".format(e)) 78 | return 79 | 80 | # Edge -> Curve (Circle) -> Location + Radius 81 | # -> Curve (Line) -> Direction 82 | 83 | items = list(map(lambda a: cur_view.getEdgeByIndex(a), indices)) 84 | 85 | points = [] 86 | directions = [] 87 | radii = [] 88 | dir = None 89 | 90 | for item in items: 91 | if item.Curve.TypeId == 'Part::GeomCircle': 92 | v1, v2 = item.Vertexes 93 | # Calculate the "direction vector" of the circle 94 | p = (item.Curve.Location - v1.Point) + (item.Curve.Location - v2.Point) 95 | p.normalize() 96 | points.append(item.Curve.Location) 97 | directions.append(p) 98 | radii.append(item.Curve.Radius) 99 | elif item.Curve.TypeId == 'Part::GeomLine': 100 | dir = item.Curve.Direction 101 | else: 102 | print("Where do I belong?", item.Curve.TypeId) 103 | 104 | if dir is None or len(points) != 2: 105 | msg("Wrong Selection", "Please select two arcs and a straight line!") 106 | return 107 | 108 | p1, p2 = points 109 | d1, d2 = directions 110 | r1, r2 = radii 111 | 112 | def dir_sign(v1, v2): 113 | """Calculate the directional sign of two vectors""" 114 | a = v1.getAngle(v2) 115 | # TODO: is this correct?! I thought that we would return 1 here and -1 otherwise... 116 | # Probably a problem because of the two coordinate system which are used in TD 117 | if a < np.pi / 2.0 or a >= (3.0/2.0) * np.pi: 118 | return -1 119 | return 1 120 | 121 | # Can be enabled to create a vertex at the arc's center 122 | #cur_view.makeCosmeticVertex(p1) 123 | #cur_view.makeCosmeticVertex(p2) 124 | 125 | # Tangential points in direction of axis 126 | vert1 = p1 + dir_sign(dir, d1) * dir * r1 127 | vert2 = p2 + dir_sign(dir, d2) * dir * r2 128 | cur_view.makeCosmeticVertex(vert1) 129 | cur_view.makeCosmeticVertex(vert2) 130 | 131 | # We can "fake" the measurement by creating the point which can be used to measure the distance 132 | distance = dir.dot(vert1 - vert2) 133 | vert3 = vert2 + (dir * distance) 134 | cur_view.makeCosmeticVertex(vert3) 135 | 136 | # Also create the other vertex, so we have an option to choose 137 | vert4 = vert1 - (dir * distance) 138 | cur_view.makeCosmeticVertex(vert4) 139 | 140 | 141 | # Recompute 142 | App.activeDocument().recompute(None,True,True) 143 | 144 | 145 | if __name__ == "__main__": 146 | find_tangential() 147 | -------------------------------------------------------------------------------- /TDThread.FCMacro: -------------------------------------------------------------------------------- 1 | """ 2 | This Macro is intended to be used in the TechDraw Workbench, 3 | it paints a thread according to DIN ISO 6410-1:1993 around a hole. 4 | 5 | THIS ONLY WORKS IN A VERY RECENT VERSION (0.19.21099)!!! 6 | """ 7 | 8 | __Name__ = 'TDThread' 9 | __Comment__ = 'Paint threads for holes in TechDraw' 10 | __Author__ = 'Sebastian Bachmann' 11 | __Version__ = '1.0' 12 | __Date__ = '2020-05-07' 13 | __License__ = 'MIT' 14 | __Web__ = 'https://github.com/reox/FreeCAD_Macros' 15 | __Wiki__ = '' 16 | __Icon__ = 'icons/TDThread.svg' 17 | __Help__ = 'Select a hole and run the macro' 18 | __Status__ = 'Stable' 19 | __Requires__ = '>=0.19.21099; py3 only' 20 | __Communication__ = 'https://github.com/reox/FreeCAD_Macros/issues' 21 | 22 | 23 | # Copyright (c) 2019, Sebastian Bachmann 24 | # 25 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 26 | # this software and associated documentation files (the "Software"), to deal in 27 | # the Software without restriction, including without limitation the rights to 28 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 29 | # the Software, and to permit persons to whom the Software is furnished to do so, 30 | # subject to the following conditions: 31 | # The above copyright notice and this permission notice shall be included in all 32 | # copies or substantial portions of the Software. 33 | # 34 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 36 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 37 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 38 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 39 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | 41 | 42 | import numpy as np 43 | from PySide.QtGui import QMessageBox 44 | from PySide import QtCore 45 | 46 | import TechDraw 47 | 48 | 49 | def msg(title, message): 50 | diag = QMessageBox(QMessageBox.Warning, title, message) 51 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 52 | diag.exec_() 53 | 54 | 55 | def paint_thread(offset=3): 56 | """ 57 | Paint a thread around a circle in TD 58 | 59 | :param float offset: additional units to draw centerlines outwards in both directions 60 | """ 61 | # FIXME: Need to get the correct values here! 62 | # Maybe one method would be to have a table of common values and select the ones which are 63 | # closest to the hole. 64 | thread_depth = 2 # additional size which will be added to the radius 65 | thread_size = 'M16' # What will be written in the dimension 66 | 67 | # Get the current view: 68 | cur_view = Gui.Selection.getSelectionEx()[0].Object 69 | 70 | if not cur_view.isDerivedFrom("TechDraw::DrawViewPart"): 71 | msg("Wrong Selection", "Must select Edges of DrawViewPart!") 72 | return 73 | 74 | # Get the names of the selected edges: 75 | elem = Gui.Selection.getSelectionEx()[0].SubElementNames 76 | 77 | try: 78 | indices = map(lambda x: int(x.replace('Edge', '')), elem) 79 | except ValueError as e: 80 | msg("Something is wrong", "Can not parse Edge indices: {}".format(e)) 81 | return 82 | 83 | # 04.1 --> dash dot, small 84 | center_style = (4, cur_view.ViewObject.IsoWidth, (0.0, 0.0, 0.0, 1.0), True) 85 | # 01.1 --> full line, small 86 | thread_style = (1, cur_view.ViewObject.IsoWidth, (0.0, 0.0, 0.0, 1.0), True) 87 | 88 | for i in indices: 89 | curve = cur_view.getEdgeByIndex(i).Curve 90 | 91 | # If we use this coordinate in makeComsmeticVertex, we get the vertex at the center. 92 | # X: positive direction to the right of the screen 93 | # Y: positive direction to the top of the screen 94 | center = curve.Location 95 | radius = curve.Radius 96 | 97 | # DIN ISO 6410-1:1993: 3/4 circle, opening and position variable 98 | edge = cur_view.makeCosmeticCircleArc(center, radius + thread_depth, 110, 20) 99 | cur_view.getCosmeticEdge(edge).Format = thread_style 100 | 101 | # Make dimension 102 | r_vec = App.Vector(radius + thread_depth, 0, 0) 103 | v1 = center + r_vec 104 | v2 = center - r_vec 105 | 106 | dim = TechDraw.makeDistanceDim(cur_view, 'DistanceX', v1, v2) 107 | # NOTE: scaling has to be applied for dimensions! 108 | dim.X = center.x * cur_view.Scale 109 | # TODO: Actually we need to take into account the size between the text and the line.. 110 | # Not sure how we can get this programatically 111 | dim.Y = (center.y + radius + 3*offset) * cur_view.Scale 112 | dim.Arbitrary = True 113 | dim.FormatSpec = thread_size 114 | 115 | # create centermark 116 | e = cur_view.makeCosmeticLine(center + App.Vector(0, radius + offset), center - App.Vector(0, radius + offset)) 117 | cur_view.getCosmeticEdge(e).Format = center_style 118 | 119 | e = cur_view.makeCosmeticLine(center + App.Vector(radius + offset, 0), center - App.Vector(radius + offset, 0)) 120 | cur_view.getCosmeticEdge(e).Format = center_style 121 | 122 | cur_view.requestPaint() 123 | 124 | # Recompute 125 | App.activeDocument().recompute(None,True,True) 126 | 127 | 128 | if __name__ == "__main__": 129 | paint_thread() 130 | -------------------------------------------------------------------------------- /Voronoi.FCMacro: -------------------------------------------------------------------------------- 1 | """ 2 | This macro is inteneded to be used in the Sketcher Workbench. 3 | 4 | The idea is to place down some points, which are the center points of the Voronoi cells. 5 | The macro will compute the convex hull of the points, a Voronoi tesselation 6 | and additionally offset all Voronoi cells in such a way, that they can be used 7 | as pockets. 8 | 9 | An additional switch exists to draw the convex hull as line segments too, which 10 | can be used to create a extrudeable sketch. 11 | 12 | Edit the settings in the main() function for the creation of the Voronoi tesselation 13 | and run the macro inside an active sketch. 14 | """ 15 | __Name__ = 'PDVoronoiFace' 16 | __Comment__ = 'Create Voronoi Pattern on Face' 17 | __Author__ = 'Sebastian Bachmann' 18 | __Version__ = '0.0.1' 19 | __Date__ = '2020-02-28' 20 | __License__ = 'MIT' 21 | __Web__ = 'https://github.com/reox/FreeCAD_Macros' 22 | __Wiki__ = '' 23 | __Icon__ = 'PDVoronoi.svg' 24 | __Help__ = 'Select a face' 25 | __Status__ = 'Beta' 26 | __Requires__ = '>=0.19.18234; py3 only' 27 | __Communication__ = 'https://github.com/reox/FreeCAD_Macros/issues' 28 | """ 29 | Copyright (c) 2019, Sebastian Bachmann 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy of 32 | this software and associated documentation files (the "Software"), to deal in 33 | the Software without restriction, including without limitation the rights to 34 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 35 | the Software, and to permit persons to whom the Software is furnished to do so, 36 | subject to the following conditions: 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 42 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 43 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 44 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 45 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 | """ 47 | import Part 48 | import numpy as np 49 | from numpy.linalg import det, norm 50 | from scipy.spatial import Voronoi, Delaunay, ConvexHull 51 | 52 | from PySide.QtGui import ( 53 | QMainWindow, 54 | QDialog, 55 | QLineEdit, 56 | QPushButton, 57 | QGridLayout, 58 | QLabel, 59 | QGroupBox, 60 | QSpacerItem, 61 | QMessageBox, 62 | ) 63 | from PySide import QtCore 64 | 65 | 66 | def msg(title, message): 67 | diag = QMessageBox(QMessageBox.Warning, title, message) 68 | diag.setWindowModality(QtCore.Qt.ApplicationModal) 69 | diag.exec_() 70 | 71 | 72 | def sum_conserving_round(inp): 73 | """ 74 | Round a list but conserve the overall sum 75 | by minimizing the roundoff error 76 | """ 77 | frac, res = np.modf(inp) 78 | items = np.argsort(frac)[::-1] 79 | for i in range(int(round(np.sum(inp) - np.sum(res)))): 80 | res[items[i]] += 1.0 81 | return res 82 | 83 | 84 | def triangle_area(v0, v1, v2): 85 | """ 86 | Returns the surface area of a triangle with given points 87 | 88 | :param np.array v0: point 1 89 | :param np.array v1: point 2 90 | :param np.array v2: point 3 91 | """ 92 | return norm(np.cross(v1 - v0, v2 - v0)) * 0.5 93 | 94 | 95 | def Det(u, v): 96 | """ 97 | Calculate the determinate of two vectors concatenated to matrix 98 | """ 99 | return det(np.vstack((u, v))) 100 | 101 | 102 | def point_in_triangle(p, v0, v1, v2): 103 | """ 104 | Checks if the point p is part of the triangle formed by v0, v1, v2 105 | v0 is a point and v1 and v2 are vectors from v0 to the other two vertices 106 | 107 | From http://mathworld.wolfram.com/TriangleInterior.html 108 | """ 109 | 110 | a = (Det(p, v2) - Det(v0, v2)) / Det(v1, v2) 111 | b = -(Det(p, v1) - Det(v0, v1)) / Det(v1, v2) 112 | 113 | return a > 0 and b > 0 and (a + b) < 1 114 | 115 | 116 | def mirror_point_at_point(p, s): 117 | """ 118 | Mirrors the point p at the point s 119 | """ 120 | return p - 2*(p - s) 121 | 122 | 123 | def fill_triangle(v0, v1, v2, n=100): 124 | """ 125 | Fills a triangle with points. 126 | The points stem from a uniform distribution. 127 | 128 | From: http://mathworld.wolfram.com/TrianglePointPicking.html 129 | """ 130 | a1 = np.random.random(n)[:, np.newaxis] 131 | a2 = np.random.random(n)[:, np.newaxis] 132 | 133 | # symmetry point at the parallelogram's edge 134 | s = v1 + ((v2 - v1) * 0.5) 135 | 136 | # Get vectors from v0 to v1 and v2 137 | v01 = (v1 - v0) 138 | v02 = (v2 - v0) 139 | 140 | x = v0 + a1 * v01 + a2 * v02 141 | x = np.array([p if point_in_triangle(p, v0, v01, v02) else mirror_point_at_point(p, s) for p in x]) 142 | return x.T 143 | 144 | """ 145 | Function by @pv (Pauli Virtanen) 146 | https://gist.github.com/pv/8036995 147 | """ 148 | def voronoi_finite_polygons_2d(vor, radius=None): 149 | """ 150 | Reconstruct infinite voronoi regions in a 2D diagram to finite 151 | regions. 152 | Parameters 153 | ---------- 154 | vor : Voronoi 155 | Input diagram 156 | radius : float, optional 157 | Distance to 'points at infinity'. 158 | Returns 159 | ------- 160 | regions : list of tuples 161 | Indices of vertices in each revised Voronoi regions. 162 | vertices : list of tuples 163 | Coordinates for revised Voronoi vertices. Same as coordinates 164 | of input vertices, with 'points at infinity' appended to the 165 | end. 166 | """ 167 | 168 | if vor.points.shape[1] != 2: 169 | raise ValueError("Requires 2D input") 170 | 171 | new_regions = [] 172 | new_vertices = vor.vertices.tolist() 173 | 174 | center = vor.points.mean(axis=0) 175 | if radius is None: 176 | radius = vor.points.ptp().max()*2 177 | 178 | # Construct a map containing all ridges for a given point 179 | all_ridges = {} 180 | for (p1, p2), (v1, v2) in zip(vor.ridge_points, vor.ridge_vertices): 181 | all_ridges.setdefault(p1, []).append((p2, v1, v2)) 182 | all_ridges.setdefault(p2, []).append((p1, v1, v2)) 183 | 184 | # Reconstruct infinite regions 185 | for p1, region in enumerate(vor.point_region): 186 | vertices = vor.regions[region] 187 | 188 | if all(v >= 0 for v in vertices): 189 | # finite region 190 | new_regions.append(vertices) 191 | continue 192 | 193 | # reconstruct a non-finite region 194 | ridges = all_ridges[p1] 195 | new_region = [v for v in vertices if v >= 0] 196 | 197 | for p2, v1, v2 in ridges: 198 | if v2 < 0: 199 | v1, v2 = v2, v1 200 | if v1 >= 0: 201 | # finite ridge: already in the region 202 | continue 203 | 204 | # Compute the missing endpoint of an infinite ridge 205 | 206 | t = vor.points[p2] - vor.points[p1] # tangent 207 | t /= np.linalg.norm(t) 208 | n = np.array([-t[1], t[0]]) # normal 209 | 210 | midpoint = vor.points[[p1, p2]].mean(axis=0) 211 | direction = np.sign(np.dot(midpoint - center, n)) * n 212 | far_point = vor.vertices[v2] + direction * radius 213 | 214 | new_region.append(len(new_vertices)) 215 | new_vertices.append(far_point.tolist()) 216 | 217 | # sort region counterclockwise 218 | vs = np.asarray([new_vertices[v] for v in new_region]) 219 | c = vs.mean(axis=0) 220 | angles = np.arctan2(vs[:,1] - c[1], vs[:,0] - c[0]) 221 | new_region = np.array(new_region)[np.argsort(angles)] 222 | 223 | # finish 224 | new_regions.append(new_region.tolist()) 225 | 226 | return new_regions, np.asarray(new_vertices) 227 | 228 | 229 | def fill_voronoi_offset(sketch, points, origin_face, offset): 230 | """ 231 | Create Voronoi cells from a list of points given by XY coordinates 232 | Z will always be set to zero. 233 | These are sketch coordinates! 234 | 235 | .. todo:: 236 | The method seems to miss out some points, which means that the surface might not completely 237 | filled with voronoi regions. 238 | 239 | :param Sketcher.SketchObject sketch: The sketch object to place the cells into 240 | :param List[Tuple[float]] points: A list of 2D points to form the centers of the cells 241 | :param Part.Face origin_face: Originating face in sketch coordinates 242 | :param float offset: the offset in Units to offset all lines. Positive offsets make the cells smaller 243 | """ 244 | vor = Voronoi(points) 245 | 246 | regions, vertices = voronoi_finite_polygons_2d(vor) 247 | 248 | # Debug: show all points 249 | #for p in points: 250 | # sketch.addGeometry(Part.Point(App.Vector(*p, 0))) 251 | 252 | for r in regions: 253 | # Simply remove points which are outside of the face... 254 | #while -1 in r: 255 | # r.remove(-1) 256 | 257 | if r == []: 258 | continue 259 | 260 | # Make a wire and calculate the offset 261 | wire = Part.makePolygon([App.Vector(*vertices[x], 0) for x in r]+ [App.Vector(*vertices[r[0]], 0)]) 262 | face = Part.makeFace(wire, 'Part::FaceMakerSimple') 263 | 264 | section = face.common(origin_face) 265 | 266 | # If the face has holes, there might be a different number of wires 267 | # than one... 268 | for new_wire in section.Wires: 269 | try: 270 | newshape = new_wire.makeOffset2D(-offset, openResult=False, intersection=True) 271 | except: 272 | print("Shape was probly too small... ignoring") 273 | else: 274 | for e in newshape.Edges: 275 | a, b = e.Vertexes 276 | sketch.addGeometry(Part.LineSegment(a.Point, b.Point)) 277 | 278 | 279 | class VoronoiForm(QDialog): 280 | def __init__(self, parent=None): 281 | super().__init__(parent) 282 | self.setWindowTitle('Voronoi Pattern on Face') 283 | 284 | layout = QGridLayout() 285 | 286 | layout.addWidget(QLabel('Points:', self), 0, 0) 287 | layout.addWidget(QLabel('Offset:', self), 1, 0) 288 | layout.addWidget(QLabel('Tessellation Tol:', self), 2, 0) 289 | layout.addWidget(QLabel('Seed:', self), 3, 0) 290 | 291 | newseed = QPushButton("Random") 292 | newseed.clicked.connect(self.setnewseed) 293 | layout.addWidget(newseed, 3, 2) 294 | 295 | self.points = QLineEdit('100') 296 | self.offset = QLineEdit('0.3') 297 | self.tol = QLineEdit('0.1') 298 | self.seed = QLineEdit('1337') 299 | 300 | layout.addWidget(self.points, 0, 1) 301 | layout.addWidget(self.offset, 1, 1) 302 | layout.addWidget(self.tol, 2, 1) 303 | layout.addWidget(self.seed, 3, 1) 304 | 305 | ok = QPushButton("OK") 306 | ok.clicked.connect(self.do) 307 | abort = QPushButton("Abort") 308 | abort.clicked.connect(self.exit) 309 | 310 | layout.addWidget(ok, 4, 0) 311 | layout.addWidget(abort, 4, 1) 312 | 313 | self.setLayout(layout) 314 | 315 | self.show() 316 | 317 | def setnewseed(self): 318 | self.seed.setText(str(np.random.randint(0, np.iinfo(np.int32).max))) 319 | 320 | @staticmethod 321 | def get_field_as(field, fun): 322 | err = False 323 | n = None 324 | try: 325 | n = fun(field.text()) 326 | except ValueError: 327 | msg('Error', 'Invalid Value "{}", must be of type {}!'.format(field.text(), fun)) 328 | err = True 329 | return n, err 330 | 331 | def do(self): 332 | err = False 333 | n, e1 = self.get_field_as(self.points, int) 334 | err |= e1 335 | offset, e1 = self.get_field_as(self.offset, float) 336 | err |= e1 337 | tol, e1 = self.get_field_as(self.tol, float) 338 | err |= e1 339 | seed, e1 = self.get_field_as(self.seed, int) 340 | 341 | if not err: 342 | np.random.seed(seed) 343 | voronoi_on_face(n, offset, tol) 344 | self.close() 345 | 346 | def exit(self, event): 347 | self.close() 348 | 349 | 350 | def voronoi_on_face(n, offset, tol): 351 | """ 352 | Create a Voronoi Pattern on a Face by creating random points. 353 | 354 | A sketch is created which contains the Voronoi cell geometry. 355 | 356 | The face is tessellated first, then the triangles are filled with random points 357 | choosen from an uniform distribution. 358 | Afterwards, each Voronoi Cell is offsetted in such a way, that ridges form between 359 | the cells. The width of each ridge is offset/2. 360 | 361 | Note, that the tessellation of the face might produce unwanted artefacts! 362 | Voronoi cells might intersect with features (holes) of the face. 363 | This can be explained by the tessellation: Consider a perfect circle, which is then 364 | tessellated. Now a finite number of triangles will be used, and the form 365 | of the circle will be approximated with a n-gon. 366 | During the calculation these n-gons are used! As the edges of the n-gon 367 | are always inside the circle, some Voronoi cells might intersect the circle. 368 | 369 | The face will in most cases also not filled completely with cells. 370 | The choise of points influences the end result. Unfortunately, I'm not entirely sure 371 | how to control the cells in such a way that cells are created on all the face. 372 | 373 | Right now, there is also no way to control the minimum size of each cell. 374 | One method might be to distribute the points more evenly - which also depends 375 | on the tessellation! 376 | 377 | :param int n: Number of points to generate 378 | :param float offset: Offset for voronoi cells, larger than 0 379 | :param float tol: Tessellation tolerance 380 | """ 381 | sel = Gui.Selection.getSelectionEx() 382 | 383 | if len(sel) != 1: 384 | msg('Error', 'Please select a single face first!') 385 | return 386 | 387 | so = sel[0].SubObjects 388 | if len(so) != 1: 389 | msg('Error', 'Please select a single face first!') 390 | return 391 | 392 | face = so[0] 393 | print(face.Wires) 394 | 395 | poly_points, simplices = face.tessellate(tol) 396 | # We need lists instead if tuples for numpy to be able to return items from 397 | # an array 398 | simplices = [list(x) for x in simplices] 399 | total_area = face.Area 400 | 401 | # Get the object which is selected 402 | feature = Gui.Selection.getSelection()[0] 403 | face_name = sel[0].SubElementNames[0] 404 | 405 | body = Gui.ActiveDocument.ActiveView.getActiveObject('pdbody') 406 | if body is None: 407 | msg('Error', 'Need an active body!') 408 | return 409 | 410 | sketch = App.ActiveDocument.addObject('Sketcher::SketchObject', 'Voronoi_Sketch') 411 | body.addObject(sketch) 412 | sketch.MapMode = 'FlatFace' 413 | sketch.Support = (feature, face_name) 414 | 415 | # We need the placement to calculate the points in the sketch... 416 | # This transform will convert all points on the surface into 2D sketch 417 | # coordinates 418 | transform = sketch.Placement.toMatrix().inverse() 419 | 420 | # NOTE: The transformation should always make the point have zero at the z 421 | # coordinate. However, sometimes the z coordinate is <<1 but not 0. 422 | # But we are confident that this should work for all faces ;) 423 | poly = np.array([[p.x, p.y] for p in [transform * p for p in poly_points]]) 424 | 425 | n_points = sum_conserving_round(np.array([triangle_area(*poly[s]) / total_area * n for s in simplices])).astype(np.uint) 426 | if np.sum(n_points) != n: 427 | msg('Warning', 'Point numbers do not match after distribution step!') 428 | 429 | points = np.empty((2, 0)) 430 | for i, s in enumerate(simplices): 431 | # If the tesselation produces too small triangles, they might not get 432 | # filled with points 433 | if n_points[i] == 0: 434 | continue 435 | points = np.hstack((points, fill_triangle(*poly[s], n=n_points[i]))) 436 | points = points.T 437 | 438 | fill_voronoi_offset(sketch, points, face.transformed(transform), offset) 439 | 440 | App.ActiveDocument.recompute() 441 | 442 | 443 | 444 | if __name__ == "__main__": 445 | form = VoronoiForm() -------------------------------------------------------------------------------- /icons/CustomFormat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | %f 71 | 72 | 73 | -------------------------------------------------------------------------------- /icons/EdgeIntersection.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 49 | 51 | 53 | 57 | 61 | 62 | 64 | 68 | 72 | 73 | 75 | 79 | 83 | 84 | 86 | 90 | 94 | 95 | 97 | 101 | 105 | 106 | 114 | 123 | 132 | 141 | 150 | 159 | 168 | 177 | 186 | 195 | 204 | 205 | 207 | 208 | 210 | image/svg+xml 211 | 213 | 214 | 215 | 216 | 217 | 220 | 230 | 233 | 236 | 239 | 244 | 249 | 250 | 254 | 262 | 270 | 278 | 286 | 294 | 295 | 299 | 307 | 315 | 323 | 331 | 339 | 340 | 341 | 342 | 343 | -------------------------------------------------------------------------------- /icons/TDChainDistances.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 34 | 36 | 40 | 44 | 45 | 47 | 51 | 55 | 56 | 65 | 75 | 78 | 82 | 86 | 87 | 96 | 103 | 114 | 123 | 133 | 143 | 153 | 163 | 173 | 183 | 184 | 207 | 209 | 210 | 212 | image/svg+xml 213 | 215 | 216 | 217 | 218 | 219 | 224 | 227 | 231 | 234 | 240 | 246 | 251 | 257 | 263 | 269 | 274 | 279 | 285 | 291 | 296 | 297 | 298 | 302 | 305 | 311 | 317 | 322 | 328 | 334 | 340 | 345 | 350 | 356 | 362 | 367 | 368 | 369 | 370 | 371 | 372 | -------------------------------------------------------------------------------- /icons/TDTangentialMeasure.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 29 | 31 | 35 | 39 | 40 | 42 | 46 | 50 | 51 | 61 | 64 | 68 | 72 | 73 | 83 | 93 | 103 | 104 | 127 | 129 | 130 | 132 | image/svg+xml 133 | 135 | 136 | 137 | 138 | 139 | 144 | 147 | 150 | 154 | 157 | 160 | 166 | 169 | 175 | 180 | 181 | 187 | 193 | 199 | 204 | 209 | 215 | 221 | 226 | 227 | 228 | 229 | 230 | 236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /icons/Voronoi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 66 | 72 | 78 | 83 | 88 | 93 | 98 | 103 | 104 | 105 | --------------------------------------------------------------------------------