├── .gitignore ├── AppStorePrivacyPolicy.txt ├── LICENSE ├── LuerFittings.manifest ├── LuerFittings.py ├── README.md └── resources ├── 16x16-disabled.png ├── 16x16.png ├── 16x16@2x.png ├── 32x32-disabled.png ├── 32x32.png ├── 32x32@2x.png ├── banner.jpg ├── icon.afphoto ├── storeicon.afphoto └── storeicon.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | .vscode 4 | -------------------------------------------------------------------------------- /AppStorePrivacyPolicy.txt: -------------------------------------------------------------------------------- 1 | This Add-In does not capture or store any user data. 2 | 3 | The Autodesk App Store captures, and gives us access to, a download record containing the following personal information: 4 | 5 | Language 6 | Country 7 | First Name 8 | Last Name 9 | Email 10 | Company Name 11 | Date & Time of download 12 | 13 | We do not store or use this data. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /LuerFittings.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "addin", 4 | "id": "c7766c33-d7e6-4fef-b141-9018de20d36f", 5 | "author": "Nico Schlueter", 6 | "description": { 7 | "": "Add-In for creating Luer Fittings. 8 | Privacy Policy: 9 | https://github.com/NicoSchlueter/LuerFittingGenerator/blob/master/AppStorePrivacyPolicy.txt" 10 | }, 11 | "version": "", 12 | "runOnStartup": false, 13 | "supportedOS": "windows|mac", 14 | "editEnabled": true 15 | } -------------------------------------------------------------------------------- /LuerFittings.py: -------------------------------------------------------------------------------- 1 | #Author-Nico Schlueter 2 | #Description-Add-In for creating Luer Fittings 3 | 4 | import adsk.core, adsk.fusion, adsk.cam, traceback 5 | import math 6 | 7 | 8 | # Global set of event handlers to keep them referenced for the duration of the command 9 | _handlers = [] 10 | 11 | COMMAND_ID = "luerFittings" 12 | COMMAND_NAME = "Luer Fitting" 13 | COMMAND_TOOLTIP = "Creates a luer fitting" 14 | 15 | # Initial persistence Dict 16 | pers = { 17 | 'DDType': "Male Slip", 18 | "VIDiametralClearance": 0, 19 | "VIHole": 0.225 20 | } 21 | 22 | # Fires when the CommandDefinition gets executed. 23 | # Responsible for adding commandInputs to the command & 24 | # registering the other command handlers. 25 | class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler): 26 | def __init__(self): 27 | super().__init__() 28 | def notify(self, args): 29 | try: 30 | global pers 31 | 32 | # Get the command that was created. 33 | cmd = adsk.core.Command.cast(args.command) 34 | 35 | # Registers the CommandExecutePreviewHandler 36 | onExecutePreview = CommandExecutePreviewHandler() 37 | cmd.executePreview.add(onExecutePreview) 38 | _handlers.append(onExecutePreview) 39 | 40 | # Registers the CommandInputChangedHandler 41 | onInputChanged = CommandInputChangedHandler() 42 | cmd.inputChanged.add(onInputChanged) 43 | _handlers.append(onInputChanged) 44 | 45 | # Registers the CommandValidateInputsEventHandler 46 | onValidate = CommandValidateInputsEventHandler() 47 | cmd.validateInputs.add(onValidate) 48 | _handlers.append(onValidate) 49 | 50 | 51 | # Get the CommandInputs collection associated with the command. 52 | inputs = cmd.commandInputs 53 | 54 | siOrigin = inputs.addSelectionInput("SIOrigin", "Point", "Select Center Point") 55 | siOrigin.addSelectionFilter("ConstructionPoints") 56 | siOrigin.addSelectionFilter("SketchPoints") 57 | siOrigin.addSelectionFilter("Vertices") 58 | siOrigin.addSelectionFilter("CircularEdges") 59 | siOrigin.setSelectionLimits(1, 1) 60 | siOrigin.tooltip = "Fitting Center Point" 61 | siOrigin.tooltipDescription = "Select the center point of the Fitting.\nWill be projected onto the plane.\n\nValid selections:\n Sketch Points\n Construction Points\n BRep Vertices\n Circular BRep Edges\n" 62 | 63 | siPlane = inputs.addSelectionInput("SIPlane", "Plane", "Select Fitting Plane") 64 | siPlane.addSelectionFilter("ConstructionPlanes") 65 | siPlane.addSelectionFilter("PlanarFaces") 66 | siPlane.setSelectionLimits(0, 1) 67 | siPlane.tooltip = "Gear Plane" 68 | siPlane.tooltipDescription = "Select the plane the fitting will be placed on.\n\nValid selections are:\n Construction Planes\n BRep Faces\n\nNot needed if SketchPoint is selected." 69 | 70 | ddType = inputs.addDropDownCommandInput("DDType", "Type", 0) 71 | ddType.listItems.add("Male Slip", pers["DDType"] == "Male Slip", "") 72 | ddType.listItems.add("Male Lock", pers["DDType"] == "Male Lock", "") 73 | ddType.listItems.add("Male Lock (internal)", pers["DDType"] == "Male Lock (internal)", "") 74 | ddType.listItems.add("Female Slip", pers["DDType"] == "Female Slip", "") 75 | ddType.listItems.add("Female Slip (internal)", pers["DDType"] == "Female Slip (internal)", "") 76 | ddType.listItems.add("Female Lock", pers["DDType"] == "Female Lock", "") 77 | 78 | viHole = inputs.addValueInput("VIHole", "Hole diameter", "mm", adsk.core.ValueInput.createByReal(pers["VIHole"])) 79 | 80 | viDiametralClearance = inputs.addValueInput("VIDiametralClearance", "Clearance (diametral)", "mm", adsk.core.ValueInput.createByReal(pers["VIDiametralClearance"])) 81 | 82 | 83 | except: 84 | print(traceback.format_exc()) 85 | 86 | 87 | 88 | # Fires when the Command is being created or when Inputs are being changed 89 | # Responsible for generating a preview of the output. 90 | # Changes done here are temporary and will be cleaned up automatically. 91 | class CommandExecutePreviewHandler(adsk.core.CommandEventHandler): 92 | def __init__(self): 93 | super().__init__() 94 | def notify(self, args): 95 | try: 96 | eventArgs = adsk.core.CommandEventArgs.cast(args) 97 | 98 | app = adsk.core.Application.get() 99 | des = app.activeProduct 100 | root = des.rootComponent 101 | comp = des.activeComponent 102 | 103 | # Saves setting to persistance dictionary 104 | global pers 105 | pers["DDType"] = args.command.commandInputs.itemById("DDType").selectedItem.name 106 | pers["VIDiametralClearance"] = args.command.commandInputs.itemById("VIDiametralClearance").value 107 | pers["VIHole"] = args.command.commandInputs.itemById("VIHole").value 108 | 109 | # Gets point object and calculates its Point3D 110 | point = args.command.commandInputs.itemById("SIOrigin").selection(0).entity 111 | pointPrim = getPrimitiveFromSelection(point) 112 | 113 | # Gets plane object or derives it from selected sketchPoint 114 | if(args.command.commandInputs.itemById("SIPlane").selectionCount == 1): 115 | plane = args.command.commandInputs.itemById("SIPlane").selection(0).entity 116 | else: 117 | plane = point.parentSketch.referencePlane 118 | 119 | # Calculates it Plane primitive 120 | planePrim = getPrimitiveFromSelection(plane) 121 | # Projects point primitive onto plane 122 | pointPrim = projectPointOnPlane(pointPrim, planePrim) 123 | 124 | # Creates a sketch on the plane object without including any geometry 125 | sketch = comp.sketches.addWithoutEdges(plane) 126 | 127 | # Gets inverse transform matrix of Sketch and transforms the point by it 128 | it = sketch.transform.copy() 129 | it.invert() 130 | pointPrim.transformBy(it) 131 | 132 | if(args.command.commandInputs.itemById("DDType").selectedItem.name == "Male Slip"): 133 | 134 | # Creates circle for base diameter of taper 135 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 136 | pointPrim, 137 | (0.4 + math.tan(math.radians(3.44)) * 0.75 - args.command.commandInputs.itemById("VIDiametralClearance").value) / 2 138 | ) 139 | 140 | # Creates circle for internal diameter 141 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 142 | pointPrim, 143 | args.command.commandInputs.itemById("VIHole").value / 2 144 | ) 145 | 146 | # Creates Object collection of both profiles 147 | oc = adsk.core.ObjectCollection.create() 148 | oc.add(sketch.profiles[0]) 149 | oc.add(sketch.profiles[1]) 150 | 151 | # Creates first extude with taper 152 | exturdeInput1 = comp.features.extrudeFeatures.createInput(oc, 0) 153 | exturdeInput1.setOneSideExtent( 154 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("7.5 mm")), 155 | 0, 156 | adsk.core.ValueInput.createByString("-1.72 deg") 157 | ) 158 | f1 = comp.features.extrudeFeatures.add(exturdeInput1) 159 | 160 | # Creates second extrude to cut internal hole 161 | exturdeInput2 = comp.features.extrudeFeatures.createInput(sketch.profiles[1], 1) 162 | exturdeInput2.setOneSideExtent( 163 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("7.5 mm")), 164 | 0, 165 | adsk.core.ValueInput.createByString("0 deg") 166 | ) 167 | f2 = comp.features.extrudeFeatures.add(exturdeInput2) 168 | 169 | if(des.designType): 170 | des.timeline.timelineGroups.add(f1.timelineObject.index-1, f2.timelineObject.index) 171 | 172 | 173 | elif(args.command.commandInputs.itemById("DDType").selectedItem.name == "Male Lock"): 174 | 175 | # Creates circle for base diameter of taper 176 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 177 | pointPrim, 178 | (0.4 + math.tan(math.radians(3.44)) * 0.75 - args.command.commandInputs.itemById("VIDiametralClearance").value) / 2 179 | ) 180 | 181 | # Creates circle for internal diameter 182 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 183 | pointPrim, 184 | args.command.commandInputs.itemById("VIHole").value / 2 185 | ) 186 | 187 | # Vector maths! Yay!!!1! 188 | offsetODArc = adsk.core.Vector3D.create(0.142, 0.3207, 0) 189 | offsetCSA1 = adsk.core.Vector3D.create(0.1417, -0.0159, 0) 190 | offsetCSA2 = adsk.core.Vector3D.create(-0.1332 , -0.0506, 0) 191 | offsetLine = adsk.core.Vector3D.create(0,0,0.55) 192 | 193 | posODArc = pointPrim.copy() 194 | posODArc.translateBy(offsetODArc) 195 | 196 | posCSA1 = pointPrim.copy() 197 | posCSA1.translateBy(offsetCSA1) 198 | 199 | posCSA2 = pointPrim.copy() 200 | posCSA2.translateBy(offsetCSA2) 201 | 202 | offsetODArc.scaleBy(-1) 203 | offsetCSA1.scaleBy(-1) 204 | offsetCSA2.scaleBy(-1) 205 | 206 | posODArc2 = pointPrim.copy() 207 | posODArc2.translateBy(offsetODArc) 208 | 209 | posCSA3 = pointPrim.copy() 210 | posCSA3.translateBy(offsetCSA1) 211 | 212 | posCSA4 = pointPrim.copy() 213 | posCSA4.translateBy(offsetCSA2) 214 | 215 | posLine = pointPrim.copy() 216 | posLine.translateBy(offsetLine) 217 | 218 | 219 | # Creates Arcs for thread crosssection 220 | odArc1 = sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 221 | pointPrim, 222 | posODArc, 223 | math.radians(42.4) 224 | ) 225 | 226 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 227 | posCSA1, 228 | odArc1.startSketchPoint, 229 | math.radians(-23) 230 | ) 231 | 232 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 233 | posCSA2, 234 | odArc1.endSketchPoint, 235 | math.radians(23) 236 | ) 237 | 238 | 239 | odArc2 = sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 240 | pointPrim, 241 | posODArc2, 242 | math.radians(42.4) 243 | ) 244 | 245 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 246 | posCSA3, 247 | odArc2.startSketchPoint, 248 | math.radians(-23) 249 | ) 250 | 251 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 252 | posCSA4, 253 | odArc2.endSketchPoint, 254 | math.radians(23) 255 | ) 256 | 257 | pathLine = sketch.sketchCurves.sketchLines.addByTwoPoints( 258 | pointPrim, 259 | posLine 260 | ) 261 | 262 | # Creates circle for internal diameter of threaded tube 263 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 264 | pointPrim, 265 | 0.4 266 | ) 267 | 268 | # Creates circle for extrenal diameter of threaded tube 269 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 270 | pointPrim, 271 | 0.5 272 | ) 273 | 274 | # Creates Object collection of both profiles 275 | oc1 = adsk.core.ObjectCollection.create() 276 | oc1.add(sketch.profiles[0]) 277 | oc1.add(sketch.profiles[1]) 278 | 279 | # Creates first extude with taper 280 | exturdeInput1 = comp.features.extrudeFeatures.createInput(oc1, 0) 281 | exturdeInput1.setOneSideExtent( 282 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("7.5 mm")), 283 | 0, 284 | adsk.core.ValueInput.createByString("-1.72 deg") 285 | ) 286 | f1 = comp.features.extrudeFeatures.add(exturdeInput1) 287 | 288 | # Creates second extrude to cut internal hole 289 | exturdeInput2 = comp.features.extrudeFeatures.createInput(sketch.profiles[1], 1) 290 | exturdeInput2.setOneSideExtent( 291 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("7.5 mm")), 292 | 0, 293 | adsk.core.ValueInput.createByString("0 deg") 294 | ) 295 | comp.features.extrudeFeatures.add(exturdeInput2) 296 | 297 | # Creates third extrude to join threaded tube 298 | exturdeInput3 = comp.features.extrudeFeatures.createInput(sketch.profiles[5], 0) 299 | exturdeInput3.setOneSideExtent( 300 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("5.5 mm")), 301 | 0, 302 | adsk.core.ValueInput.createByString("0 deg") 303 | ) 304 | comp.features.extrudeFeatures.add(exturdeInput3) 305 | 306 | # Creates Object collection of thread wings 307 | oc2 = adsk.core.ObjectCollection.create() 308 | oc2.add(sketch.profiles[2]) 309 | oc2.add(sketch.profiles[4]) 310 | 311 | path = comp.features.createPath(pathLine) 312 | sweepInput = comp.features.sweepFeatures.createInput(oc2, path, 0) 313 | sweepInput.twistAngle = adsk.core.ValueInput.createByReal(math.radians(396)) 314 | f2 = comp.features.sweepFeatures.add(sweepInput) 315 | 316 | if(des.designType): 317 | des.timeline.timelineGroups.add(f1.timelineObject.index-1, f2.timelineObject.index) 318 | 319 | 320 | elif(args.command.commandInputs.itemById("DDType").selectedItem.name == "Male Lock (internal)"): 321 | 322 | pointPrim.translateBy(adsk.core.Vector3D.create(0,0,-0.55)) 323 | 324 | 325 | # Creates circle for base diameter of taper 326 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 327 | pointPrim, 328 | (0.4 + math.tan(math.radians(3.44)) * 0.75 - args.command.commandInputs.itemById("VIDiametralClearance").value) / 2 329 | ) 330 | 331 | # Creates circle for internal diameter 332 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 333 | pointPrim, 334 | args.command.commandInputs.itemById("VIHole").value / 2 335 | ) 336 | 337 | # Vector maths! Yay!!!1! 338 | offsetODArc = adsk.core.Vector3D.create(0.142, 0.3207, 0) 339 | offsetCSA1 = adsk.core.Vector3D.create(0.1417, -0.0159, 0) 340 | offsetCSA2 = adsk.core.Vector3D.create(-0.1332 , -0.0506, 0) 341 | offsetLine = adsk.core.Vector3D.create(0,0,0.55) 342 | 343 | posODArc = pointPrim.copy() 344 | posODArc.translateBy(offsetODArc) 345 | 346 | posCSA1 = pointPrim.copy() 347 | posCSA1.translateBy(offsetCSA1) 348 | 349 | posCSA2 = pointPrim.copy() 350 | posCSA2.translateBy(offsetCSA2) 351 | 352 | offsetODArc.scaleBy(-1) 353 | offsetCSA1.scaleBy(-1) 354 | offsetCSA2.scaleBy(-1) 355 | 356 | posODArc2 = pointPrim.copy() 357 | posODArc2.translateBy(offsetODArc) 358 | 359 | posCSA3 = pointPrim.copy() 360 | posCSA3.translateBy(offsetCSA1) 361 | 362 | posCSA4 = pointPrim.copy() 363 | posCSA4.translateBy(offsetCSA2) 364 | 365 | posLine = pointPrim.copy() 366 | posLine.translateBy(offsetLine) 367 | 368 | 369 | # Creates Arcs for thread crosssection 370 | odArc1 = sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 371 | pointPrim, 372 | posODArc, 373 | math.radians(42.4) 374 | ) 375 | 376 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 377 | posCSA1, 378 | odArc1.startSketchPoint, 379 | math.radians(-23) 380 | ) 381 | 382 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 383 | posCSA2, 384 | odArc1.endSketchPoint, 385 | math.radians(23) 386 | ) 387 | 388 | 389 | odArc2 = sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 390 | pointPrim, 391 | posODArc2, 392 | math.radians(42.4) 393 | ) 394 | 395 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 396 | posCSA3, 397 | odArc2.startSketchPoint, 398 | math.radians(-23) 399 | ) 400 | 401 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 402 | posCSA4, 403 | odArc2.endSketchPoint, 404 | math.radians(23) 405 | ) 406 | 407 | pathLine = sketch.sketchCurves.sketchLines.addByTwoPoints( 408 | pointPrim, 409 | posLine 410 | ) 411 | 412 | # Creates circle for internal diameter of threaded tube 413 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 414 | pointPrim, 415 | 0.4 416 | ) 417 | 418 | # Creates circle for extrenal diameter of threaded tube 419 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 420 | pointPrim, 421 | 0.5 422 | ) 423 | 424 | # Creates Object collection of thread wings 425 | oc2 = adsk.core.ObjectCollection.create() 426 | oc2.add(sketch.profiles[0]) 427 | oc2.add(sketch.profiles[1]) 428 | oc2.add(sketch.profiles[3]) 429 | 430 | path = comp.features.createPath(pathLine) 431 | sweepInput = comp.features.sweepFeatures.createInput(oc2, path, 1) 432 | sweepInput.twistAngle = adsk.core.ValueInput.createByReal(math.radians(396)) 433 | f1 = comp.features.sweepFeatures.add(sweepInput) 434 | 435 | 436 | 437 | 438 | # Creates Object collection of both profiles 439 | oc1 = adsk.core.ObjectCollection.create() 440 | oc1.add(sketch.profiles[0]) 441 | oc1.add(sketch.profiles[1]) 442 | 443 | # Creates first extude with taper 444 | exturdeInput1 = comp.features.extrudeFeatures.createInput(oc1, 0) 445 | exturdeInput1.setOneSideExtent( 446 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("7.5 mm")), 447 | 0, 448 | adsk.core.ValueInput.createByString("-1.72 deg") 449 | ) 450 | comp.features.extrudeFeatures.add(exturdeInput1) 451 | 452 | # Creates second extrude to cut internal hole 453 | exturdeInput2 = comp.features.extrudeFeatures.createInput(sketch.profiles[1], 1) 454 | exturdeInput2.setOneSideExtent( 455 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("7.5 mm")), 456 | 0, 457 | adsk.core.ValueInput.createByString("0 deg") 458 | ) 459 | f2 = comp.features.extrudeFeatures.add(exturdeInput2) 460 | 461 | if(des.designType): 462 | des.timeline.timelineGroups.add(f1.timelineObject.index-1, f2.timelineObject.index) 463 | 464 | 465 | elif(args.command.commandInputs.itemById("DDType").selectedItem.name == "Female Slip"): 466 | 467 | # Creates circle for outside diameter 468 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 469 | pointPrim, 470 | 0.65/2 471 | ) 472 | 473 | # Creates circle for base diameter of taper 474 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 475 | pointPrim, 476 | (0.43 - math.tan(math.radians(3.44)) * 0.9 + args.command.commandInputs.itemById("VIDiametralClearance").value) / 2 477 | ) 478 | 479 | # Creates extude cut with taper 480 | exturdeInput1 = comp.features.extrudeFeatures.createInput(sketch.profiles[0], 0) 481 | exturdeInput1.setOneSideExtent( 482 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("9 mm")), 483 | 0, 484 | adsk.core.ValueInput.createByString("0 deg") 485 | ) 486 | f1 = comp.features.extrudeFeatures.add(exturdeInput1) 487 | 488 | # Creates extude cut with taper 489 | exturdeInput2 = comp.features.extrudeFeatures.createInput(sketch.profiles[1], 1) 490 | exturdeInput2.setOneSideExtent( 491 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("9 mm")), 492 | 0, 493 | adsk.core.ValueInput.createByString("1.72 deg") 494 | ) 495 | f2 = comp.features.extrudeFeatures.add(exturdeInput2) 496 | 497 | if(des.designType): 498 | des.timeline.timelineGroups.add(f1.timelineObject.index-1, f2.timelineObject.index) 499 | 500 | 501 | elif(args.command.commandInputs.itemById("DDType").selectedItem.name == "Female Slip (internal)"): 502 | 503 | # Creates circle for base diameter of taper 504 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 505 | pointPrim, 506 | (0.43 + args.command.commandInputs.itemById("VIDiametralClearance").value) / 2 507 | ) 508 | 509 | # Creates extude cut with taper 510 | exturdeInput1 = comp.features.extrudeFeatures.createInput(sketch.profiles[0], 1) 511 | exturdeInput1.setOneSideExtent( 512 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("-9 mm")), 513 | 0, 514 | adsk.core.ValueInput.createByString("-1.72 deg") 515 | ) 516 | f1 = comp.features.extrudeFeatures.add(exturdeInput1) 517 | 518 | if(des.designType): 519 | des.timeline.timelineGroups.add(f1.timelineObject.index-1, f1.timelineObject.index) 520 | 521 | 522 | elif(args.command.commandInputs.itemById("DDType").selectedItem.name == "Female Lock"): 523 | 524 | # Creates circle for outside diameter 525 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 526 | pointPrim, 527 | 0.67/2 528 | ) 529 | 530 | # Creates circle for base diameter of taper 531 | sketch.sketchCurves.sketchCircles.addByCenterRadius( 532 | pointPrim, 533 | (0.43 - math.tan(math.radians(3.44)) * 0.9 + args.command.commandInputs.itemById("VIDiametralClearance").value) / 2 534 | ) 535 | 536 | # Vector maths! Yay!!!1! 537 | offsetODArc = adsk.core.Vector3D.create(-0.124, 0.3695, 0) 538 | offsetCSA1 = adsk.core.Vector3D.create(-0.1487, 0.0435, 0) 539 | offsetCSA2 = adsk.core.Vector3D.create(-0.0156 , 0.1534, 0) 540 | offsetLine = adsk.core.Vector3D.create(0,0,0.9) 541 | 542 | posODArc = pointPrim.copy() 543 | posODArc.translateBy(offsetODArc) 544 | 545 | posCSA1 = pointPrim.copy() 546 | posCSA1.translateBy(offsetCSA1) 547 | 548 | posCSA2 = pointPrim.copy() 549 | posCSA2.translateBy(offsetCSA2) 550 | 551 | offsetODArc.scaleBy(-1) 552 | offsetCSA1.scaleBy(-1) 553 | offsetCSA2.scaleBy(-1) 554 | 555 | posODArc2 = pointPrim.copy() 556 | posODArc2.translateBy(offsetODArc) 557 | 558 | posCSA3 = pointPrim.copy() 559 | posCSA3.translateBy(offsetCSA1) 560 | 561 | posCSA4 = pointPrim.copy() 562 | posCSA4.translateBy(offsetCSA2) 563 | 564 | posLine = pointPrim.copy() 565 | posLine.translateBy(offsetLine) 566 | 567 | 568 | # Creates Arcs for thread crosssection 569 | odArc1 = sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 570 | pointPrim, 571 | posODArc, 572 | math.radians(42.4) 573 | ) 574 | 575 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 576 | posCSA1, 577 | odArc1.startSketchPoint, 578 | math.radians(-22.8) 579 | ) 580 | 581 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 582 | posCSA2, 583 | odArc1.endSketchPoint, 584 | math.radians(22.8) 585 | ) 586 | 587 | 588 | odArc2 = sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 589 | pointPrim, 590 | posODArc2, 591 | math.radians(42.4) 592 | ) 593 | 594 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 595 | posCSA3, 596 | odArc2.startSketchPoint, 597 | math.radians(-22.8) 598 | ) 599 | 600 | sketch.sketchCurves.sketchArcs.addByCenterStartSweep( 601 | posCSA4, 602 | odArc2.endSketchPoint, 603 | math.radians(22.8) 604 | ) 605 | 606 | pathLine = sketch.sketchCurves.sketchLines.addByTwoPoints( 607 | pointPrim, 608 | posLine 609 | ) 610 | 611 | 612 | # Creates extude cut with taper 613 | exturdeInput1 = comp.features.extrudeFeatures.createInput(sketch.profiles[3], 0) 614 | exturdeInput1.setOneSideExtent( 615 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("9 mm")), 616 | 0, 617 | adsk.core.ValueInput.createByString("0 deg") 618 | ) 619 | f1 = comp.features.extrudeFeatures.add(exturdeInput1) 620 | 621 | # Creates extude cut with taper 622 | exturdeInput2 = comp.features.extrudeFeatures.createInput(sketch.profiles[0], 1) 623 | exturdeInput2.setOneSideExtent( 624 | adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByString("9 mm")), 625 | 0, 626 | adsk.core.ValueInput.createByString("1.72 deg") 627 | ) 628 | comp.features.extrudeFeatures.add(exturdeInput2) 629 | 630 | # Creates Object collection of both thread wings 631 | oc = adsk.core.ObjectCollection.create() 632 | oc.add(sketch.profiles[1]) 633 | oc.add(sketch.profiles[2]) 634 | 635 | path = comp.features.createPath(pathLine) 636 | sweepInput = comp.features.sweepFeatures.createInput(oc, path, 0) 637 | sweepInput.twistAngle = adsk.core.ValueInput.createByReal(math.radians(648)) 638 | f2 = comp.features.sweepFeatures.add(sweepInput) 639 | 640 | if(des.designType): 641 | des.timeline.timelineGroups.add(f1.timelineObject.index-1, f2.timelineObject.index) 642 | 643 | eventArgs.isValidResult = True 644 | 645 | except: 646 | print(traceback.format_exc()) 647 | 648 | 649 | # Fires when CommandInputs are changed 650 | # Responsible for dynamically updating other Command Inputs 651 | class CommandInputChangedHandler(adsk.core.InputChangedEventHandler): 652 | def __init__(self): 653 | super().__init__() 654 | def notify(self, args): 655 | try: 656 | if(args.input.id == "DDType"): 657 | args.inputs.itemById("VIHole").isVisible = not args.input.selectedItem.name[0] == "F" 658 | except: 659 | print(traceback.format_exc()) 660 | 661 | 662 | # Fires when CommandInputs are changed or other parts of the UI are updated 663 | # Responsible for turning the ok button on or off and allowing preview 664 | class CommandValidateInputsEventHandler(adsk.core.ValidateInputsEventHandler): 665 | def __init__(self): 666 | super().__init__() 667 | 668 | def notify(self, args): 669 | try: 670 | app = adsk.core.Application.get() 671 | des = app.activeProduct 672 | 673 | args.areInputsValid = True 674 | 675 | siOrigin = args.inputs.itemById("SIOrigin") 676 | siPlane = args.inputs.itemById("SIPlane") 677 | 678 | if(siOrigin.selectionCount == 1 and siPlane.selectionCount == 0): 679 | if(not ( siOrigin.selection(0).entity.objectType == "adsk::fusion::SketchPoint" ) or des.designType == 0): 680 | args.areInputsValid = False 681 | except: 682 | print(traceback.format_exc()) 683 | 684 | 685 | def getPrimitiveFromSelection(selection): 686 | # Construction Plane 687 | if selection.objectType == "adsk::fusion::ConstructionPlane": 688 | # TODO: Coordinate in assembly context, world transform still required! 689 | return selection.geometry 690 | 691 | # Sketch Profile 692 | if selection.objectType == "adsk::fusion::Profile": 693 | return adsk.core.Plane.createUsingDirections( 694 | selection.parentSketch.origin, 695 | selection.parentSketch.xDirection, 696 | selection.parentSketch.yDirection 697 | ) 698 | 699 | # BRepFace 700 | if selection.objectType == "adsk::fusion::BRepFace": 701 | _, normal = selection.evaluator.getNormalAtPoint(selection.pointOnFace) 702 | return adsk.core.Plane.create( 703 | selection.pointOnFace, 704 | normal 705 | ) 706 | 707 | # Construction Axis 708 | if selection.objectType == "adsk::fusion::ConstructionAxis": 709 | # TODO: Coordinate in assembly context, world transform still required! 710 | return selection.geometry 711 | 712 | # BRepEdge 713 | if selection.objectType == "adsk::fusion::BRepEdge": 714 | # Linear edge 715 | if (selection.geometry.objectType == "adsk::core::Line3D"): 716 | _, tangent = selection.evaluator.getTangent(0) 717 | return adsk.core.InfiniteLine3D.create( 718 | selection.pointOnEdge, 719 | tangent 720 | ) 721 | # Circular edge 722 | if (selection.geometry.objectType in ["adsk::core::Circle3D", "adsk::core::Arc3D"]): 723 | return selection.geometry.center 724 | 725 | # Sketch Line 726 | if selection.objectType == "adsk::fusion::SketchLine": 727 | return selection.worldGeometry.asInfiniteLine() 728 | 729 | # Construction Point 730 | if selection.objectType == "adsk::fusion::ConstructionPoint": 731 | # TODO: Coordinate in assembly context, world transform still required! 732 | return selection.geometry 733 | 734 | # Sketch Point 735 | if selection.objectType == "adsk::fusion::SketchPoint": 736 | return selection.worldGeometry 737 | 738 | # BRepVertex 739 | if selection.objectType == "adsk::fusion::BRepVertex": 740 | return selection.geometry 741 | 742 | 743 | def projectPointOnPlane(point, plane): 744 | originToPoint = plane.origin.vectorTo(point) 745 | 746 | normal = plane.normal.copy() 747 | normal.normalize() 748 | distPtToPln = normal.dotProduct(originToPoint) 749 | 750 | normal.scaleBy(-distPtToPln) 751 | 752 | ptOnPln = point.copy() 753 | ptOnPln.translateBy(normal) 754 | 755 | return ptOnPln 756 | 757 | 758 | def run(context): 759 | try: 760 | 761 | app = adsk.core.Application.get() 762 | ui = app.userInterface 763 | 764 | commandDefinitions = ui.commandDefinitions 765 | #check the command exists or not 766 | cmdDef = commandDefinitions.itemById(COMMAND_ID) 767 | if not cmdDef: 768 | cmdDef = commandDefinitions.addButtonDefinition(COMMAND_ID, COMMAND_NAME, 769 | COMMAND_TOOLTIP, 'resources') 770 | 771 | ui.allToolbarPanels.itemById("SolidCreatePanel").controls.addCommand(cmdDef, "FusionThreadCommand", False) 772 | 773 | onCommandCreated = CommandCreatedHandler() 774 | cmdDef.commandCreated.add(onCommandCreated) 775 | _handlers.append(onCommandCreated) 776 | except: 777 | print(traceback.format_exc()) 778 | 779 | 780 | def stop(context): 781 | try: 782 | app = adsk.core.Application.get() 783 | ui = app.userInterface 784 | 785 | #Removes the commandDefinition from the toolbar 786 | p = ui.allToolbarPanels.itemById("SolidCreatePanel").controls.itemById(COMMAND_ID) 787 | if p: 788 | p.deleteMe() 789 | 790 | #Deletes the commandDefinition 791 | ui.commandDefinitions.itemById(COMMAND_ID).deleteMe() 792 | 793 | 794 | 795 | except: 796 | print(traceback.format_exc()) 797 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luer Fitting Generator 2 | 3 | Add-In for Autodesk Fusion360. 4 | Generates male and female luer lock and slip fittings. 5 | 6 | 7 | 8 |
9 | 10 | ![banner](https://user-images.githubusercontent.com/30301307/81482946-ce974500-923a-11ea-8e94-88fddddf7b5e.jpg) 11 | 12 | 13 |
14 | 15 | # Installation 16 | 17 | * Download the Project as ZIP and extract it somewhere you can find again, but won't bother you. (or use git to clone it there) 18 | * Open Fusion360 and press ADD-INS > Scripts and Add-ins 19 | * Select the tab Add-Ins and click the green plus symbol next to "My Add-Ins" 20 | * Navigate to the extracted Project forlder and hit open 21 | * The Add-in should now appear in the "My Add-Ins" list. Select it in the list. If desired check the "Run ond Startup" checkbox and hit run. 22 | * The Command will appear as CREATE > Luer Fitting 23 | 24 | -------------------------------------------------------------------------------- /resources/16x16-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/16x16-disabled.png -------------------------------------------------------------------------------- /resources/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/16x16.png -------------------------------------------------------------------------------- /resources/16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/16x16@2x.png -------------------------------------------------------------------------------- /resources/32x32-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/32x32-disabled.png -------------------------------------------------------------------------------- /resources/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/32x32.png -------------------------------------------------------------------------------- /resources/32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/32x32@2x.png -------------------------------------------------------------------------------- /resources/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/banner.jpg -------------------------------------------------------------------------------- /resources/icon.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/icon.afphoto -------------------------------------------------------------------------------- /resources/storeicon.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/storeicon.afphoto -------------------------------------------------------------------------------- /resources/storeicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phossystems/LuerFittingGenerator/c0faca16f572ef178c004e276b81cd9b5b0a487c/resources/storeicon.png --------------------------------------------------------------------------------